diff --git a/.version b/.version index ccbccc3d..276cbf9e 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.2.0 +2.3.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 72f15441..715ee6ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## v2.3.0 + +### Bug Fixes + +- ENCRYPTION_KEY needed for version and help commands ([#1256](https://github.com/pocket-id/pocket-id/pull/1256) by @kmendell) +- prevent deletion of OIDC provider logo for non admin/anonymous users ([#1267](https://github.com/pocket-id/pocket-id/pull/1267) by @HiMoritz) +- add `type="url"` to url inputs ([bb7b0d5](https://github.com/pocket-id/pocket-id/commit/bb7b0d56084df49b6a003cc3eaf076884e2cbf60) by @stonith404) +- increase rate limit for frontend and api requests ([aab7e36](https://github.com/pocket-id/pocket-id/commit/aab7e364e85f1ce13950da93cc50324328cdd96d) by @stonith404) +- decode URL-encoded client ID and secret in Basic auth ([#1263](https://github.com/pocket-id/pocket-id/pull/1263) by @ypomortsev) +- token endpoint must not accept params as query string args ([#1321](https://github.com/pocket-id/pocket-id/pull/1321) by @ItalyPaleAle) +- left align input error messages ([b3fe143](https://github.com/pocket-id/pocket-id/commit/b3fe14313684f9d8c389ed93ea8e479e3681b5c6) by @stonith404) +- disallow API key renewal and creation with API key authentication ([#1334](https://github.com/pocket-id/pocket-id/pull/1334) by @stonith404) + +### Features + +- add VERSION_CHECK_DISABLED environment variable ([#1254](https://github.com/pocket-id/pocket-id/pull/1254) by @dihmandrake) +- add support for HTTP/2 ([56afebc](https://github.com/pocket-id/pocket-id/commit/56afebc242be7ed14b58185425d6445bf18f640a) by @stonith404) +- manageability of uncompressed geolite db file ([#1234](https://github.com/pocket-id/pocket-id/pull/1234) by @gucheen) +- add JWT ID for generated tokens ([#1322](https://github.com/pocket-id/pocket-id/pull/1322) by @imnotjames) +- current version api endpoint ([#1310](https://github.com/pocket-id/pocket-id/pull/1310) by @kmendell) + +### Other + +- bump @sveltejs/kit from 2.49.2 to 2.49.5 in the npm_and_yarn group across 1 directory ([#1240](https://github.com/pocket-id/pocket-id/pull/1240) by @dependabot[bot]) +- bump svelte from 5.46.1 to 5.46.4 in the npm_and_yarn group across 1 directory ([#1242](https://github.com/pocket-id/pocket-id/pull/1242) by @dependabot[bot]) +- bump devalue to 5.6.2 ([9dbc02e](https://github.com/pocket-id/pocket-id/commit/9dbc02e56871b2de6a39c443e1455efc26a949f7) by @kmendell) +- upgrade deps ([4811625](https://github.com/pocket-id/pocket-id/commit/4811625cdd64b47ea67b7a9b03396e455896ccd6) by @kmendell) +- add Estonian files ([53ef61a](https://github.com/pocket-id/pocket-id/commit/53ef61a3e5c4b77edec49d41ab94302bfec84269) by @kmendell) +- update AAGUIDs ([#1257](https://github.com/pocket-id/pocket-id/pull/1257) by @github-actions[bot]) +- add Norwegian language files ([80558c5](https://github.com/pocket-id/pocket-id/commit/80558c562533e7b4d658d5baa4221d8cd209b47d) by @stonith404) +- run formatter ([60825c5](https://github.com/pocket-id/pocket-id/commit/60825c5743b0e233ab622fd4d0ea04eb7ab59529) by @kmendell) +- bump axios from 1.13.2 to 1.13.5 in the npm_and_yarn group across 1 directory ([#1309](https://github.com/pocket-id/pocket-id/pull/1309) by @dependabot[bot]) +- update dependenicies ([94a4897](https://github.com/pocket-id/pocket-id/commit/94a48977ba24e099b6221838d620c365eb1d4bf4) by @kmendell) +- update AAGUIDs ([#1316](https://github.com/pocket-id/pocket-id/pull/1316) by @github-actions[bot]) +- bump svelte from 5.46.4 to 5.51.5 in the npm_and_yarn group across 1 directory ([#1324](https://github.com/pocket-id/pocket-id/pull/1324) by @dependabot[bot]) +- bump @sveltejs/kit from 2.49.5 to 2.52.2 in the npm_and_yarn group across 1 directory ([#1327](https://github.com/pocket-id/pocket-id/pull/1327) by @dependabot[bot]) +- upgrade dependencies ([0678699](https://github.com/pocket-id/pocket-id/commit/0678699d0cce5448c425b2c16bedab5fc242cbf0) by @stonith404) +- upgrade to node 24 and go 1.26.0 ([#1328](https://github.com/pocket-id/pocket-id/pull/1328) by @kmendell) + +**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.2.0...v2.3.0 + ## v2.2.0 ### Bug Fixes diff --git a/backend/frontend/frontend_included.go b/backend/frontend/frontend_included.go index ac1f6b2e..6486671b 100644 --- a/backend/frontend/frontend_included.go +++ b/backend/frontend/frontend_included.go @@ -8,8 +8,10 @@ import ( "fmt" "io" "io/fs" + "mime" "net/http" "os" + "path" "strings" "time" @@ -58,9 +60,16 @@ func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc) e return fmt.Errorf("failed to create sub FS: %w", err) } - cacheMaxAge := time.Hour * 24 - fileServer := NewFileServerWithCaching(http.FS(distFS), int(cacheMaxAge.Seconds())) + // Load a map of all files to see which ones are available pre-compressed + preCompressed, err := listPreCompressedAssets(distFS) + if err != nil { + return fmt.Errorf("failed to index pre-compressed frontend assets: %w", err) + } + // Init the file server + fileServer := NewFileServerWithCaching(http.FS(distFS), preCompressed) + + // Handler for Gin handler := func(c *gin.Context) { path := strings.TrimPrefix(c.Request.URL.Path, "/") @@ -108,34 +117,138 @@ func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc) e type FileServerWithCaching struct { root http.FileSystem lastModified time.Time - cacheMaxAge int lastModifiedHeaderValue string - cacheControlHeaderValue string + preCompressed preCompressedMap } -func NewFileServerWithCaching(root http.FileSystem, maxAge int) *FileServerWithCaching { +func NewFileServerWithCaching(root http.FileSystem, preCompressed preCompressedMap) *FileServerWithCaching { return &FileServerWithCaching{ root: root, lastModified: time.Now(), - cacheMaxAge: maxAge, lastModifiedHeaderValue: time.Now().UTC().Format(http.TimeFormat), - cacheControlHeaderValue: fmt.Sprintf("public, max-age=%d", maxAge), + preCompressed: preCompressed, } } func (f *FileServerWithCaching) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Check if the client has a cached version - if ifModifiedSince := r.Header.Get("If-Modified-Since"); ifModifiedSince != "" { - ifModifiedSinceTime, err := time.Parse(http.TimeFormat, ifModifiedSince) - if err == nil && f.lastModified.Before(ifModifiedSinceTime.Add(1*time.Second)) { - // Client's cached version is up to date - w.WriteHeader(http.StatusNotModified) - return + // First, set cache headers + // Check if the request is for an immutable asset + if isImmutableAsset(r) { + // Set the cache control header as immutable with a long expiration + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } else { + // Check if the client has a cached version + ifModifiedSince := r.Header.Get("If-Modified-Since") + if ifModifiedSince != "" { + ifModifiedSinceTime, err := time.Parse(http.TimeFormat, ifModifiedSince) + if err == nil && f.lastModified.Before(ifModifiedSinceTime.Add(1*time.Second)) { + // Client's cached version is up to date + w.WriteHeader(http.StatusNotModified) + return + } + } + + // Cache other assets for up to 24 hours, but set Last-Modified too + w.Header().Set("Last-Modified", f.lastModifiedHeaderValue) + w.Header().Set("Cache-Control", "public, max-age=86400") + } + + // Check if the asset is available pre-compressed + _, ok := f.preCompressed[r.URL.Path] + if ok { + // Add a "Vary" with "Accept-Encoding" so CDNs are aware that content is pre-compressed + w.Header().Add("Vary", "Accept-Encoding") + + // Select the encoding if any + ext, ce := f.selectEncoding(r) + if ext != "" { + // Set the content type explicitly before changing the path + ct := mime.TypeByExtension(path.Ext(r.URL.Path)) + if ct != "" { + w.Header().Set("Content-Type", ct) + } + + // Make the serve return the encoded content + w.Header().Set("Content-Encoding", ce) + r.URL.Path += "." + ext } } - w.Header().Set("Last-Modified", f.lastModifiedHeaderValue) - w.Header().Set("Cache-Control", f.cacheControlHeaderValue) - http.FileServer(f.root).ServeHTTP(w, r) } + +func (f *FileServerWithCaching) selectEncoding(r *http.Request) (ext string, contentEnc string) { + available, ok := f.preCompressed[r.URL.Path] + if !ok { + return "", "" + } + + // Check if the client accepts compressed files + acceptEncoding := strings.TrimSpace(strings.ToLower(r.Header.Get("Accept-Encoding"))) + if acceptEncoding == "" { + return "", "" + } + + // Prefer brotli over gzip when both are accepted. + if available.br && (acceptEncoding == "*" || acceptEncoding == "br" || strings.Contains(acceptEncoding, "br")) { + return "br", "br" + } + if available.gz && (acceptEncoding == "gzip" || strings.Contains(acceptEncoding, "gzip")) { + return "gz", "gzip" + } + + return "", "" +} + +func isImmutableAsset(r *http.Request) bool { + switch { + // Fonts + case strings.HasPrefix(r.URL.Path, "/fonts/"): + return true + + // Compiled SvelteKit assets + case strings.HasPrefix(r.URL.Path, "/_app/immutable/"): + return true + + default: + return false + } +} + +type preCompressedMap map[string]struct { + br bool + gz bool +} + +func listPreCompressedAssets(distFS fs.FS) (preCompressedMap, error) { + preCompressed := make(preCompressedMap, 0) + err := fs.WalkDir(distFS, ".", func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + switch { + case strings.HasSuffix(path, ".br"): + originalPath := "/" + strings.TrimSuffix(path, ".br") + entry := preCompressed[originalPath] + entry.br = true + preCompressed[originalPath] = entry + case strings.HasSuffix(path, ".gz"): + originalPath := "/" + strings.TrimSuffix(path, ".gz") + entry := preCompressed[originalPath] + entry.gz = true + preCompressed[originalPath] = entry + } + + return nil + }) + if err != nil { + return nil, err + } + + return preCompressed, nil +} diff --git a/backend/go.mod b/backend/go.mod index 9013dc1e..28b6a327 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,6 +12,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec github.com/disintegration/imaging v1.6.2 + github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 github.com/emersion/go-smtp v0.24.0 github.com/gin-contrib/slog v1.2.0 @@ -74,6 +75,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.14.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect @@ -123,6 +125,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nlnwa/whatwg-url v0.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect diff --git a/backend/go.sum b/backend/go.sum index 49d3a7a9..4486468e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -46,6 +46,9 @@ github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA= +github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= @@ -88,6 +91,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1 h1:RW22Y3QjGrb97NUA8yupdFcaqg//+hMI2fZrETBvQ4s= +github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1/go.mod h1:mnVcdqOeYg0HvT6veRo7wINa1mJ+lC/R4ig2lWcapSI= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= @@ -257,6 +262,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nlnwa/whatwg-url v0.5.0 h1:l71cqfqG44+VCQZQX3wD4bwheFWicPxuwaCimLEfpDo= +github.com/nlnwa/whatwg-url v0.5.0/go.mod h1:X/ejnFFVbaOWdSul+cnlsSHviCzGZJdvPkgc9zD8IY8= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -320,6 +327,7 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/otelslog v0.15.0 h1:yOYhGNPZseueTTvWp5iBD3/CthrmvayUXYEX862dDi4= @@ -385,6 +393,9 @@ golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= @@ -392,34 +403,65 @@ golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkN golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index 13fb9a13..9a0b41b0 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -20,7 +20,7 @@ type AlreadyInUseError struct { func (e *AlreadyInUseError) Error() string { return e.Property + " is already in use" } -func (e *AlreadyInUseError) HttpStatusCode() int { return 400 } +func (e *AlreadyInUseError) HttpStatusCode() int { return http.StatusBadRequest } func (e *AlreadyInUseError) Is(target error) bool { // Ignore the field property when checking if an error is of the type AlreadyInUseError @@ -31,26 +31,26 @@ func (e *AlreadyInUseError) Is(target error) bool { type SetupAlreadyCompletedError struct{} func (e *SetupAlreadyCompletedError) Error() string { return "setup already completed" } -func (e *SetupAlreadyCompletedError) HttpStatusCode() int { return 400 } +func (e *SetupAlreadyCompletedError) HttpStatusCode() int { return http.StatusConflict } type TokenInvalidOrExpiredError struct{} func (e *TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" } -func (e *TokenInvalidOrExpiredError) HttpStatusCode() int { return 400 } +func (e *TokenInvalidOrExpiredError) HttpStatusCode() int { return http.StatusUnauthorized } type DeviceCodeInvalid struct{} func (e *DeviceCodeInvalid) Error() string { return "one time access code must be used on the device it was generated for" } -func (e *DeviceCodeInvalid) HttpStatusCode() int { return 400 } +func (e *DeviceCodeInvalid) HttpStatusCode() int { return http.StatusUnauthorized } type TokenInvalidError struct{} func (e *TokenInvalidError) Error() string { return "Token is invalid" } -func (e *TokenInvalidError) HttpStatusCode() int { return 400 } +func (e *TokenInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } type OidcMissingAuthorizationError struct{} @@ -60,46 +60,51 @@ func (e *OidcMissingAuthorizationError) HttpStatusCode() int { return http.Statu type OidcGrantTypeNotSupportedError struct{} func (e *OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" } -func (e *OidcGrantTypeNotSupportedError) HttpStatusCode() int { return 400 } +func (e *OidcGrantTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest } type OidcMissingClientCredentialsError struct{} func (e *OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" } -func (e *OidcMissingClientCredentialsError) HttpStatusCode() int { return 400 } +func (e *OidcMissingClientCredentialsError) HttpStatusCode() int { return http.StatusBadRequest } type OidcClientSecretInvalidError struct{} func (e *OidcClientSecretInvalidError) Error() string { return "invalid client secret" } -func (e *OidcClientSecretInvalidError) HttpStatusCode() int { return 400 } +func (e *OidcClientSecretInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } type OidcClientAssertionInvalidError struct{} func (e *OidcClientAssertionInvalidError) Error() string { return "invalid client assertion" } -func (e *OidcClientAssertionInvalidError) HttpStatusCode() int { return 400 } +func (e *OidcClientAssertionInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } type OidcInvalidAuthorizationCodeError struct{} func (e *OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" } -func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return 400 } +func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest } + +type OidcClientNotFoundError struct{} + +func (e *OidcClientNotFoundError) Error() string { return "client not found" } +func (e *OidcClientNotFoundError) HttpStatusCode() int { return http.StatusNotFound } type OidcMissingCallbackURLError struct{} func (e *OidcMissingCallbackURLError) Error() string { return "unable to detect callback url, it might be necessary for an admin to fix this" } -func (e *OidcMissingCallbackURLError) HttpStatusCode() int { return 400 } +func (e *OidcMissingCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } type OidcInvalidCallbackURLError struct{} func (e *OidcInvalidCallbackURLError) Error() string { return "invalid callback URL, it might be necessary for an admin to fix this" } -func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return 400 } +func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } type FileTypeNotSupportedError struct{} func (e *FileTypeNotSupportedError) Error() string { return "file type not supported" } -func (e *FileTypeNotSupportedError) HttpStatusCode() int { return 400 } +func (e *FileTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest } type FileTooLargeError struct { MaxSize string @@ -280,6 +285,13 @@ func (e *APIKeyExpirationDateError) Error() string { } func (e *APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest } +type APIKeyAuthNotAllowedError struct{} + +func (e *APIKeyAuthNotAllowedError) Error() string { + return "API key authentication is not allowed for this endpoint" +} +func (e *APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden } + type OidcInvalidRefreshTokenError struct{} func (e *OidcInvalidRefreshTokenError) Error() string { diff --git a/backend/internal/controller/api_key_controller.go b/backend/internal/controller/api_key_controller.go index 3b364682..0162b96b 100644 --- a/backend/internal/controller/api_key_controller.go +++ b/backend/internal/controller/api_key_controller.go @@ -26,12 +26,11 @@ func NewApiKeyController(group *gin.RouterGroup, authMiddleware *middleware.Auth uc := &ApiKeyController{apiKeyService: apiKeyService} apiKeyGroup := group.Group("/api-keys") - apiKeyGroup.Use(authMiddleware.WithAdminNotRequired().Add()) { - apiKeyGroup.GET("", uc.listApiKeysHandler) - apiKeyGroup.POST("", uc.createApiKeyHandler) - apiKeyGroup.POST("/:id/renew", uc.renewApiKeyHandler) - apiKeyGroup.DELETE("/:id", uc.revokeApiKeyHandler) + apiKeyGroup.GET("", authMiddleware.WithAdminNotRequired().Add(), uc.listApiKeysHandler) + apiKeyGroup.POST("", authMiddleware.WithAdminNotRequired().WithApiKeyAuthDisabled().Add(), uc.createApiKeyHandler) + apiKeyGroup.POST("/:id/renew", authMiddleware.WithAdminNotRequired().WithApiKeyAuthDisabled().Add(), uc.renewApiKeyHandler) + apiKeyGroup.DELETE("/:id", authMiddleware.WithAdminNotRequired().Add(), uc.revokeApiKeyHandler) } } diff --git a/backend/internal/controller/oidc_controller.go b/backend/internal/controller/oidc_controller.go index 5dd9404f..193a6723 100644 --- a/backend/internal/controller/oidc_controller.go +++ b/backend/internal/controller/oidc_controller.go @@ -335,11 +335,13 @@ func (oc *OidcController) introspectTokenHandler(c *gin.Context) { ) creds.ClientID, creds.ClientSecret, ok = utils.OAuthClientBasicAuth(c.Request) if !ok { - // If there's no basic auth, check if we have a bearer token + // If there's no basic auth, check if we have a bearer token (used as client assertion) bearer, ok := utils.BearerAuth(c.Request) if ok { creds.ClientAssertionType = service.ClientAssertionTypeJWTBearer creds.ClientAssertion = bearer + // When using client assertions, client_id can be passed as a form field + creds.ClientID = input.ClientID } } @@ -662,8 +664,13 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) { } func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) { + // Per RFC 8628 (OAuth 2.0 Device Authorization Grant), parameters for the device authorization request MUST be sent in the body of the POST request + // Gin's "ShouldBind" by default reads from the query string too, so we need to reset all query string args before invoking ShouldBind + c.Request.URL.RawQuery = "" + var input dto.OidcDeviceAuthorizationRequestDto - if err := c.ShouldBind(&input); err != nil { + err := c.ShouldBind(&input) + if err != nil { _ = c.Error(err) return } diff --git a/backend/internal/dto/oidc_dto.go b/backend/internal/dto/oidc_dto.go index 08e271bb..e6a186a6 100644 --- a/backend/internal/dto/oidc_dto.go +++ b/backend/internal/dto/oidc_dto.go @@ -98,7 +98,8 @@ type OidcCreateTokensDto struct { } type OidcIntrospectDto struct { - Token string `form:"token" binding:"required"` + Token string `form:"token" binding:"required"` + ClientID string `form:"client_id"` } type OidcUpdateAllowedUserGroupsDto struct { diff --git a/backend/internal/dto/signup_dto.go b/backend/internal/dto/signup_dto.go index b135a49f..f2ab2c55 100644 --- a/backend/internal/dto/signup_dto.go +++ b/backend/internal/dto/signup_dto.go @@ -3,7 +3,7 @@ package dto type SignUpDto struct { Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"` Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"` - FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"` + FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"` LastName string `json:"lastName" binding:"max=50" unorm:"nfc"` Token string `json:"token"` } diff --git a/backend/internal/dto/user_dto.go b/backend/internal/dto/user_dto.go index 90e457ff..000b5381 100644 --- a/backend/internal/dto/user_dto.go +++ b/backend/internal/dto/user_dto.go @@ -26,9 +26,9 @@ type UserCreateDto struct { Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"` Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"` EmailVerified bool `json:"emailVerified"` - FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"` + FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"` LastName string `json:"lastName" binding:"max=50" unorm:"nfc"` - DisplayName string `json:"displayName" binding:"required,min=1,max=100" unorm:"nfc"` + DisplayName string `json:"displayName" binding:"max=100" unorm:"nfc"` IsAdmin bool `json:"isAdmin"` Locale *string `json:"locale"` Disabled bool `json:"disabled"` diff --git a/backend/internal/dto/user_dto_test.go b/backend/internal/dto/user_dto_test.go index 251dfece..62dd6188 100644 --- a/backend/internal/dto/user_dto_test.go +++ b/backend/internal/dto/user_dto_test.go @@ -33,14 +33,24 @@ func TestUserCreateDto_Validate(t *testing.T) { }, wantErr: "Field validation for 'Username' failed on the 'required' tag", }, + { + name: "missing first name", + input: UserCreateDto{ + Username: "testuser", + Email: new("test@example.com"), + LastName: "Doe", + }, + wantErr: "", + }, { name: "missing display name", input: UserCreateDto{ + Username: "testuser", Email: new("test@example.com"), FirstName: "John", LastName: "Doe", }, - wantErr: "Field validation for 'DisplayName' failed on the 'required' tag", + wantErr: "", }, { name: "username contains invalid characters", @@ -73,7 +83,7 @@ func TestUserCreateDto_Validate(t *testing.T) { LastName: "Doe", DisplayName: "John Doe", }, - wantErr: "Field validation for 'FirstName' failed on the 'required' tag", + wantErr: "", }, { name: "last name too long", diff --git a/backend/internal/dto/validations.go b/backend/internal/dto/validations.go index 8aca787a..135706fa 100644 --- a/backend/internal/dto/validations.go +++ b/backend/internal/dto/validations.go @@ -1,9 +1,7 @@ package dto import ( - "net/url" "regexp" - "strings" "time" "github.com/pocket-id/pocket-id/backend/internal/utils" @@ -67,19 +65,6 @@ func ValidateClientID(clientID string) bool { // ValidateCallbackURL validates callback URLs with support for wildcards func ValidateCallbackURL(raw string) bool { - // Don't validate if it contains a wildcard - if strings.Contains(raw, "*") { - return true - } - - u, err := url.Parse(raw) - if err != nil { - return false - } - - if !u.IsAbs() { - return false - } - - return true + err := utils.ValidateCallbackURLPattern(raw) + return err == nil } diff --git a/backend/internal/middleware/auth_middleware.go b/backend/internal/middleware/auth_middleware.go index c66b54f1..3af9ce17 100644 --- a/backend/internal/middleware/auth_middleware.go +++ b/backend/internal/middleware/auth_middleware.go @@ -18,6 +18,7 @@ type AuthMiddleware struct { type AuthOptions struct { AdminRequired bool SuccessOptional bool + AllowApiKeyAuth bool } func NewAuthMiddleware( @@ -31,6 +32,7 @@ func NewAuthMiddleware( options: AuthOptions{ AdminRequired: true, SuccessOptional: false, + AllowApiKeyAuth: true, }, } } @@ -59,6 +61,17 @@ func (m *AuthMiddleware) WithSuccessOptional() *AuthMiddleware { return clone } +// WithApiKeyAuthDisabled disables API key authentication fallback and requires JWT auth. +func (m *AuthMiddleware) WithApiKeyAuthDisabled() *AuthMiddleware { + clone := &AuthMiddleware{ + apiKeyMiddleware: m.apiKeyMiddleware, + jwtMiddleware: m.jwtMiddleware, + options: m.options, + } + clone.options.AllowApiKeyAuth = false + return clone +} + func (m *AuthMiddleware) Add() gin.HandlerFunc { return func(c *gin.Context) { userID, isAdmin, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired) @@ -79,6 +92,21 @@ func (m *AuthMiddleware) Add() gin.HandlerFunc { return } + if !m.options.AllowApiKeyAuth { + if m.options.SuccessOptional { + c.Next() + return + } + + c.Abort() + if c.GetHeader("X-API-Key") != "" { + _ = c.Error(&common.APIKeyAuthNotAllowedError{}) + return + } + _ = c.Error(err) + return + } + // JWT auth failed, try API key auth userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired) if err == nil { diff --git a/backend/internal/middleware/auth_middleware_test.go b/backend/internal/middleware/auth_middleware_test.go new file mode 100644 index 00000000..e3fb0ee6 --- /dev/null +++ b/backend/internal/middleware/auth_middleware_test.go @@ -0,0 +1,104 @@ +package middleware + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/common" + "github.com/pocket-id/pocket-id/backend/internal/dto" + "github.com/pocket-id/pocket-id/backend/internal/model" + datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" + "github.com/pocket-id/pocket-id/backend/internal/service" + testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" +) + +func TestWithApiKeyAuthDisabled(t *testing.T) { + gin.SetMode(gin.TestMode) + + originalEnvConfig := common.EnvConfig + defer func() { + common.EnvConfig = originalEnvConfig + }() + common.EnvConfig.AppURL = "https://test.example.com" + common.EnvConfig.EncryptionKey = []byte("0123456789abcdef0123456789abcdef") + + db := testutils.NewDatabaseForTest(t) + + appConfigService, err := service.NewAppConfigService(t.Context(), db) + require.NoError(t, err) + + jwtService, err := service.NewJwtService(t.Context(), db, appConfigService) + require.NoError(t, err) + + userService := service.NewUserService(db, jwtService, nil, nil, appConfigService, nil, nil, nil, nil) + apiKeyService, err := service.NewApiKeyService(t.Context(), db, nil) + require.NoError(t, err) + + authMiddleware := NewAuthMiddleware(apiKeyService, userService, jwtService) + + user := createUserForAuthMiddlewareTest(t, db) + jwtToken, err := jwtService.GenerateAccessToken(user) + require.NoError(t, err) + + _, apiKeyToken, err := apiKeyService.CreateApiKey(t.Context(), user.ID, dto.ApiKeyCreateDto{ + Name: "Middleware API Key", + ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)), + }) + require.NoError(t, err) + + router := gin.New() + router.Use(NewErrorHandlerMiddleware().Add()) + router.GET("/api/protected", authMiddleware.WithAdminNotRequired().WithApiKeyAuthDisabled().Add(), func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) + + t.Run("rejects API key auth when API key auth is disabled", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/protected", nil) + req.Header.Set("X-API-Key", apiKeyToken) + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, req) + + require.Equal(t, http.StatusForbidden, recorder.Code) + + var body map[string]string + err := json.Unmarshal(recorder.Body.Bytes(), &body) + require.NoError(t, err) + require.Equal(t, "API key authentication is not allowed for this endpoint", body["error"]) + }) + + t.Run("allows JWT auth when API key auth is disabled", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/protected", nil) + req.Header.Set("Authorization", "Bearer "+jwtToken) + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, req) + + require.Equal(t, http.StatusNoContent, recorder.Code) + }) +} + +func createUserForAuthMiddlewareTest(t *testing.T, db *gorm.DB) model.User { + t.Helper() + + email := "auth@example.com" + user := model.User{ + Username: "auth-user", + Email: &email, + FirstName: "Auth", + LastName: "User", + DisplayName: "Auth User", + } + + err := db.Create(&user).Error + require.NoError(t, err) + + return user +} diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 4426512a..bfd90cc2 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -39,7 +39,7 @@ func (u User) WebAuthnDisplayName() string { if u.DisplayName != "" { return u.DisplayName } - return u.FirstName + " " + u.LastName + return u.FullName() } func (u User) WebAuthnIcon() string { return "" } @@ -76,7 +76,16 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential } func (u User) FullName() string { - return u.FirstName + " " + u.LastName + fullname := strings.TrimSpace(u.FirstName + " " + u.LastName) + if fullname != "" { + return fullname + } + + if u.DisplayName != "" { + return u.DisplayName + } + + return u.Username } func (u User) Initials() string { diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index 0d175a92..3c29d6e9 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -77,6 +77,9 @@ func (s *ApiKeyService) CreateApiKey(ctx context.Context, userID string, input d Create(&apiKey). Error if err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return model.ApiKey{}, "", &common.AlreadyInUseError{Property: "API key name"} + } return model.ApiKey{}, "", err } diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index 828e0710..1d04c8d1 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -1644,34 +1644,19 @@ func clientAuthCredentialsFromCreateTokensDto(d *dto.OidcCreateTokensDto) Client } func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, tx *gorm.DB, input ClientAuthCredentials, allowPublicClientsWithoutAuth bool) (client *model.OidcClient, err error) { - isClientAssertion := input.ClientAssertionType == ClientAssertionTypeJWTBearer && input.ClientAssertion != "" - - // Determine the client ID based on the authentication method - var clientID string - switch { - case isClientAssertion: - // Extract client ID from the JWT assertion's 'sub' claim - clientID, err = s.extractClientIDFromAssertion(input.ClientAssertion) - if err != nil { - slog.Error("Failed to extract client ID from assertion", "error", err) - return nil, &common.OidcClientAssertionInvalidError{} - } - case input.ClientID != "": - // Use the provided client ID for other authentication methods - clientID = input.ClientID - default: + if input.ClientID == "" { return nil, &common.OidcMissingClientCredentialsError{} } // Load the OIDC client's configuration err = tx. WithContext(ctx). - First(&client, "id = ?", clientID). + First(&client, "id = ?", input.ClientID). Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) && isClientAssertion { - return nil, &common.OidcClientAssertionInvalidError{} - } + if errors.Is(err, gorm.ErrRecordNotFound) { + slog.WarnContext(ctx, "Client not found", slog.String("client", input.ClientID)) + return nil, &common.OidcClientNotFoundError{} + } else if err != nil { return nil, err } @@ -1686,7 +1671,7 @@ func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, tx *g return client, nil // Next, check if we want to use client assertions from federated identities - case isClientAssertion: + case input.ClientAssertionType == ClientAssertionTypeJWTBearer && input.ClientAssertion != "": err = s.verifyClientAssertionFromFederatedIdentities(ctx, client, input) if err != nil { slog.WarnContext(ctx, "Invalid assertion for client", slog.String("client", client.ID), slog.Any("error", err)) @@ -1783,36 +1768,20 @@ func (s *OidcService) verifyClientAssertionFromFederatedIdentities(ctx context.C // (Note: we don't use jwt.WithIssuer() because that would be redundant) _, err = jwt.Parse(assertion, jwt.WithValidate(true), + jwt.WithAcceptableSkew(clockSkew), jwt.WithKeySet(jwks, jws.WithInferAlgorithmFromKey(true), jws.WithUseDefault(true)), jwt.WithAudience(audience), jwt.WithSubject(subject), ) if err != nil { - return fmt.Errorf("client assertion is not valid: %w", err) + return fmt.Errorf("client assertion could not be verified: %w", err) } // If we're here, the assertion is valid return nil } -// extractClientIDFromAssertion extracts the client_id from the JWT assertion's 'sub' claim -func (s *OidcService) extractClientIDFromAssertion(assertion string) (string, error) { - // Parse the JWT without verification first to get the claims - insecureToken, err := jwt.ParseInsecure([]byte(assertion)) - if err != nil { - return "", fmt.Errorf("failed to parse JWT assertion: %w", err) - } - - // Extract the subject claim which must be the client_id according to RFC 7523 - sub, ok := insecureToken.Subject() - if !ok || sub == "" { - return "", fmt.Errorf("missing or invalid 'sub' claim in JWT assertion") - } - - return sub, nil -} - func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, userID string, scopes []string) (*dto.OidcClientPreviewDto, error) { tx := s.db.Begin() defer func() { diff --git a/backend/internal/service/oidc_service_test.go b/backend/internal/service/oidc_service_test.go index 4dbab3f6..4374a430 100644 --- a/backend/internal/service/oidc_service_test.go +++ b/backend/internal/service/oidc_service_test.go @@ -229,6 +229,12 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) { Subject: federatedClient.ID, JWKS: federatedClientIssuer + "/jwks.json", }, + { + Issuer: "federated-issuer-2", + Audience: federatedClientAudience, + Subject: "my-federated-client", + JWKS: federatedClientIssuer + "/jwks.json", + }, {Issuer: federatedClientIssuerDefaults}, }, }, @@ -461,6 +467,43 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) { // Generate a token input := dto.OidcCreateTokensDto{ + ClientID: federatedClient.ID, + ClientAssertion: string(signedToken), + ClientAssertionType: ClientAssertionTypeJWTBearer, + } + createdToken, err := s.createTokenFromClientCredentials(t.Context(), input) + require.NoError(t, err) + require.NotNil(t, token) + + // Verify the token + claims, err := s.jwtService.VerifyOAuthAccessToken(createdToken.AccessToken) + require.NoError(t, err, "Failed to verify generated token") + + // Check the claims + subject, ok := claims.Subject() + _ = assert.True(t, ok, "User ID not found in token") && + assert.Equal(t, "client-"+federatedClient.ID, subject, "Token subject should match federated client ID with prefix") + audience, ok := claims.Audience() + _ = assert.True(t, ok, "Audience not found in token") && + assert.Equal(t, []string{federatedClient.ID}, audience, "Audience should contain the federated client ID") + }) + + t.Run("Succeeds with valid assertion and custom subject", func(t *testing.T) { + // Create JWT for federated identity + token, err := jwt.NewBuilder(). + Issuer("federated-issuer-2"). + Audience([]string{federatedClientAudience}). + Subject("my-federated-client"). + IssuedAt(time.Now()). + Expiration(time.Now().Add(10 * time.Minute)). + Build() + require.NoError(t, err) + signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES256(), privateJWK)) + require.NoError(t, err) + + // Generate a token + input := dto.OidcCreateTokensDto{ + ClientID: federatedClient.ID, ClientAssertion: string(signedToken), ClientAssertionType: ClientAssertionTypeJWTBearer, } @@ -483,6 +526,7 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) { t.Run("Fails with invalid assertion", func(t *testing.T) { input := dto.OidcCreateTokensDto{ + ClientID: confidentialClient.ID, ClientAssertion: "invalid.jwt.token", ClientAssertionType: ClientAssertionTypeJWTBearer, } diff --git a/backend/internal/utils/callback_url_util.go b/backend/internal/utils/callback_url_util.go index 4ce7e98b..7fa44a97 100644 --- a/backend/internal/utils/callback_url_util.go +++ b/backend/internal/utils/callback_url_util.go @@ -1,14 +1,31 @@ package utils import ( + "log/slog" "net" "net/url" "path" - "regexp" + "strconv" "strings" + + "github.com/dunglas/go-urlpattern" ) -// GetCallbackURLFromList returns the first callback URL that matches the input callback URL +// ValidateCallbackURLPattern checks if the given callback URL pattern +// is valid according to the rules defined in this package. +func ValidateCallbackURLPattern(pattern string) error { + if pattern == "*" { + return nil + } + + pattern, _, _ = strings.Cut(pattern, "#") + pattern = normalizeToURLPatternStandard(pattern) + + _, err := urlpattern.New(pattern, "", nil) + return err +} + +// GetCallbackURLFromList returns the first callback URL that matches the input callback URL. func GetCallbackURLFromList(urls []string, inputCallbackURL string) (callbackURL string, err error) { // Special case for Loopback Interface Redirection. Quoting from RFC 8252 section 7.3: // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 @@ -17,17 +34,7 @@ func GetCallbackURLFromList(urls []string, inputCallbackURL string) (callbackURL // time of the request for loopback IP redirect URIs, to accommodate // clients that obtain an available ephemeral port from the operating // system at the time of the request. - loopbackCallbackURLWithoutPort := "" - u, _ := url.Parse(inputCallbackURL) - - if u != nil && u.Scheme == "http" { - host := u.Hostname() - ip := net.ParseIP(host) - if host == "localhost" || (ip != nil && ip.IsLoopback()) { - u.Host = host - loopbackCallbackURLWithoutPort = u.String() - } - } + loopbackCallbackURLWithoutPort := loopbackURLWithWildcardPort(inputCallbackURL) for _, pattern := range urls { // Try the original callback first @@ -54,6 +61,28 @@ func GetCallbackURLFromList(urls []string, inputCallbackURL string) (callbackURL return "", nil } +func loopbackURLWithWildcardPort(input string) string { + u, _ := url.Parse(input) + + if u == nil || u.Scheme != "http" { + return "" + } + + host := u.Hostname() + ip := net.ParseIP(host) + if host != "localhost" && (ip == nil || !ip.IsLoopback()) { + return "" + } + + // For IPv6 loopback hosts, brackets are required when serializing without a port. + if strings.Contains(host, ":") { + u.Host = "[" + host + "]" + } else { + u.Host = host + } + return u.String() +} + // matchCallbackURL checks if the input callback URL matches the given pattern. // It supports wildcard matching for paths and query parameters. // @@ -64,143 +93,176 @@ func matchCallbackURL(pattern string, inputCallbackURL string) (matches bool, er return true, nil } - // Strip fragment part + // Strip fragment part. // The endpoint URI MUST NOT include a fragment component. // https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2 pattern, _, _ = strings.Cut(pattern, "#") inputCallbackURL, _, _ = strings.Cut(inputCallbackURL, "#") // Store and strip query part - var patternQuery url.Values - if i := strings.Index(pattern, "?"); i >= 0 { - patternQuery, err = url.ParseQuery(pattern[i+1:]) - if err != nil { - return false, err - } - pattern = pattern[:i] - } - var inputQuery url.Values - if i := strings.Index(inputCallbackURL, "?"); i >= 0 { - inputQuery, err = url.ParseQuery(inputCallbackURL[i+1:]) - if err != nil { - return false, err - } - inputCallbackURL = inputCallbackURL[:i] - } - - // Split both pattern and input parts - patternParts, patternPath := splitParts(pattern) - inputParts, inputPath := splitParts(inputCallbackURL) - - // Verify everything except the path and query parameters - if len(patternParts) != len(inputParts) { - return false, nil - } - - for i, patternPart := range patternParts { - matched, err := path.Match(patternPart, inputParts[i]) - if err != nil || !matched { - return false, err - } - } - - // Verify path with wildcard support - matched, err := matchPath(patternPath, inputPath) - if err != nil || !matched { + pattern, patternQuery, err := extractQueryParams(pattern) + if err != nil { return false, err } - // Verify query parameters - if len(patternQuery) != len(inputQuery) { + inputCallbackURL, inputQuery, err := extractQueryParams(inputCallbackURL) + if err != nil { + return false, err + } + + pattern = normalizeToURLPatternStandard(pattern) + + // Validate query params + v := validateQueryParams(patternQuery, inputQuery) + if !v { return false, nil } + // Validate the rest of the URL using urlpattern + p, err := urlpattern.New(pattern, "", nil) + if err != nil { + //nolint:nilerr + slog.Warn("invalid callback URL pattern, skipping", "pattern", pattern, "error", err) + return false, nil + } + + return p.Test(inputCallbackURL, ""), nil +} + +// normalizeToURLPatternStandard converts patterns with single asterisk wildcards and globstar wildcards +// into a format that can be parsed by the urlpattern package, which uses :param for single segment wildcards +// and ** for multi-segment wildcards. +// Additionally, it escapes ":" with a backslash inside IPv6 addresses +func normalizeToURLPatternStandard(pattern string) string { + patternBase, patternPath := extractPath(pattern) + + var result strings.Builder + result.Grow(len(pattern) + 5) // Add 5 for some extra capacity, hoping to avoid many re-allocations + + // First, process the base + + // 0 = scheme + // 1 = hostname (optionally with username/password) - before IPv6 start (no `[` found) + // 2 = is matching IPv6 (until `]`) + // 3 = after hostname + var step int + for i := 0; i < len(patternBase); i++ { + switch step { + case 0: + if i > 3 && patternBase[i] == '/' && patternBase[i-1] == '/' && patternBase[i-2] == ':' { + // We just passed the scheme + step = 1 + } + case 1: + switch patternBase[i] { + case '/', ']': + // No IPv6, skip to end of this logic + step = 3 + case '[': + // Start of IPv6 match + step = 2 + } + case 2: + if patternBase[i] == '/' || patternBase[i] == ']' || patternBase[i] == '[' { + // End of IPv6 match + step = 3 + } + + switch patternBase[i] { + case ':': + // We are matching an IPv6 block and there's a colon, so escape that + result.WriteByte('\\') + case '/', ']', '[': + // End of IPv6 match + step = 3 + } + } + + // Write the byte + result.WriteByte(patternBase[i]) + } + + // Next, process the path + for i := 0; i < len(patternPath); i++ { + if patternPath[i] == '*' { + // Replace globstar with a single asterisk + if i+1 < len(patternPath) && patternPath[i+1] == '*' { + result.WriteString("*") + i++ // skip next * + } else { + // Replace single asterisk with :p{index} + result.WriteString(":p") + result.WriteString(strconv.Itoa(i)) + } + } else { + // Add the byte + result.WriteByte(patternPath[i]) + } + } + return result.String() +} + +func extractPath(url string) (base string, path string) { + pathStart := -1 + + // Look for scheme:// first + i := strings.Index(url, "://") + if i >= 0 { + // Look for the next slash after scheme:// + rest := url[i+3:] + if j := strings.IndexByte(rest, '/'); j >= 0 { + pathStart = i + 3 + j + } + } else { + // Otherwise, first slash is path start + pathStart = strings.IndexByte(url, '/') + } + + if pathStart >= 0 { + path = url[pathStart:] + base = url[:pathStart] + } else { + path = "" + base = url + } + + return base, path +} + +func extractQueryParams(rawUrl string) (base string, query url.Values, err error) { + if i := strings.IndexByte(rawUrl, '?'); i >= 0 { + query, err = url.ParseQuery(rawUrl[i+1:]) + if err != nil { + return "", nil, err + } + rawUrl = rawUrl[:i] + } + + return rawUrl, query, nil +} + +func validateQueryParams(patternQuery, inputQuery url.Values) bool { + if len(patternQuery) != len(inputQuery) { + return false + } + for patternKey, patternValues := range patternQuery { inputValues, exists := inputQuery[patternKey] if !exists { - return false, nil + return false } if len(patternValues) != len(inputValues) { - return false, nil + return false } for i := range patternValues { matched, err := path.Match(patternValues[i], inputValues[i]) if err != nil || !matched { - return false, err + return false } } } - return true, nil -} - -// matchPath matches the input path against the pattern with wildcard support -// Supported wildcards: -// -// '*' matches any sequence of characters except '/' -// '**' matches any sequence of characters including '/' -func matchPath(pattern string, input string) (matches bool, err error) { - var regexPattern strings.Builder - regexPattern.WriteString("^") - - runes := []rune(pattern) - n := len(runes) - - for i := 0; i < n; { - switch runes[i] { - case '*': - // Check if it's a ** (globstar) - if i+1 < n && runes[i+1] == '*' { - // globstar = .* (match slashes too) - regexPattern.WriteString(".*") - i += 2 - } else { - // single * = [^/]* (no slash) - regexPattern.WriteString(`[^/]*`) - i++ - } - default: - regexPattern.WriteString(regexp.QuoteMeta(string(runes[i]))) - i++ - } - } - - regexPattern.WriteString("$") - - matched, err := regexp.MatchString(regexPattern.String(), input) - return matched, err -} - -// splitParts splits the URL into parts by special characters and returns the path separately -func splitParts(s string) (parts []string, path string) { - split := func(r rune) bool { - return r == ':' || r == '/' || r == '[' || r == ']' || r == '@' || r == '.' - } - - pathStart := -1 - - // Look for scheme:// first - if i := strings.Index(s, "://"); i >= 0 { - // Look for the next slash after scheme:// - rest := s[i+3:] - if j := strings.IndexRune(rest, '/'); j >= 0 { - pathStart = i + 3 + j - } - } else { - // Otherwise, first slash is path start - pathStart = strings.IndexRune(s, '/') - } - - if pathStart >= 0 { - path = s[pathStart:] - base := s[:pathStart] - parts = strings.FieldsFunc(base, split) - } else { - parts = strings.FieldsFunc(s, split) - path = "" - } - - return parts, path + return true } diff --git a/backend/internal/utils/callback_url_util_test.go b/backend/internal/utils/callback_url_util_test.go index 959a8789..9e109620 100644 --- a/backend/internal/utils/callback_url_util_test.go +++ b/backend/internal/utils/callback_url_util_test.go @@ -7,6 +7,142 @@ import ( "github.com/stretchr/testify/require" ) +func TestValidateCallbackURLPattern(t *testing.T) { + tests := []struct { + name string + pattern string + shouldError bool + }{ + { + name: "exact URL", + pattern: "https://example.com/callback", + shouldError: false, + }, + { + name: "wildcard scheme", + pattern: "*://example.com/callback", + shouldError: false, + }, + { + name: "wildcard port", + pattern: "https://example.com:*/callback", + shouldError: false, + }, + { + name: "partial wildcard port", + pattern: "https://example.com:80*/callback", + shouldError: false, + }, + { + name: "wildcard userinfo", + pattern: "https://user:*@example.com/callback", + shouldError: false, + }, + { + name: "glob wildcard", + pattern: "*", + shouldError: false, + }, + { + name: "relative URL", + pattern: "/callback", + shouldError: true, + }, + { + name: "missing scheme separator", + pattern: "https//example.com/callback", + shouldError: true, + }, + { + name: "malformed wildcard host glob", + pattern: "https://exa[mple.com/callback", + shouldError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateCallbackURLPattern(tt.pattern) + if tt.shouldError { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +func TestNormalizeToURLPatternStandard(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "exact URL unchanged", + input: "https://example.com/callback", + expected: "https://example.com/callback", + }, + { + name: "single wildcard path segment converted to named parameter", + input: "https://example.com/api/*/callback", + expected: "https://example.com/api/:p5/callback", + }, + { + name: "single wildcard in path suffix converted to named parameter", + input: "https://example.com/test*", + expected: "https://example.com/test:p5", + }, + { + name: "globstar converted to single asterisk", + input: "https://example.com/**/callback", + expected: "https://example.com/*/callback", + }, + { + name: "mixed globstar and single wildcard conversion", + input: "https://example.com/**/v1/**/callback/*", + expected: "https://example.com/*/v1/*/callback/:p19", + }, + { + name: "URL without path unchanged", + input: "https://example.com", + expected: "https://example.com", + }, + { + name: "relative path conversion", + input: "/foo/*/bar", + expected: "/foo/:p5/bar", + }, + { + name: "wildcard in hostname is not normalized by this function", + input: "https://*.example.com/callback", + expected: "https://*.example.com/callback", + }, + { + name: "IPv6 hostname escapes all colons inside address", + input: "https://[2001:db8:1:1::a:1]/callback", + expected: "https://[2001\\:db8\\:1\\:1\\:\\:a\\:1]/callback", + }, + { + name: "IPv6 hostname with port escapes only address colons", + input: "https://[::1]:8080/callback", + expected: "https://[\\:\\:1]:8080/callback", + }, + { + name: "wildcard in query is converted when query is part of input", + input: "https://example.com/callback?code=*", + expected: "https://example.com/callback?code=:p15", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, normalizeToURLPatternStandard(tt.input)) + }) + } +} + func TestMatchCallbackURL(t *testing.T) { tests := []struct { name string @@ -27,6 +163,18 @@ func TestMatchCallbackURL(t *testing.T) { "https://example.com/callback", false, }, + { + "exact match - IPv4", + "https://10.1.0.1/callback", + "https://10.1.0.1/callback", + true, + }, + { + "exact match - IPv6", + "https://[2001:db8:1:1::a:1]/callback", + "https://[2001:db8:1:1::a:1]/callback", + true, + }, // Scheme { @@ -111,6 +259,30 @@ func TestMatchCallbackURL(t *testing.T) { "https://example.com:8080/callback", true, }, + { + "wildcard port - IPv4", + "https://10.1.0.1:*/callback", + "https://10.1.0.1:8080/callback", + true, + }, + { + "partial wildcard in port prefix - IPv4", + "https://10.1.0.1:80*/callback", + "https://10.1.0.1:8080/callback", + true, + }, + { + "wildcard port - IPv6", + "https://[2001:db8:1:1::a:1]:*/callback", + "https://[2001:db8:1:1::a:1]:8080/callback", + true, + }, + { + "partial wildcard in port prefix - IPv6", + "https://[2001:db8:1:1::a:1]:80*/callback", + "https://[2001:db8:1:1::a:1]:8080/callback", + true, + }, // Path { @@ -131,6 +303,18 @@ func TestMatchCallbackURL(t *testing.T) { "https://example.com/callback", true, }, + { + "wildcard entire path - IPv4", + "https://10.1.0.1/*", + "https://10.1.0.1/callback", + true, + }, + { + "wildcard entire path - IPv6", + "https://[2001:db8:1:1::a:1]/*", + "https://[2001:db8:1:1::a:1]/callback", + true, + }, { "partial wildcard in path prefix", "https://example.com/test*", @@ -187,12 +371,6 @@ func TestMatchCallbackURL(t *testing.T) { "https://example.com/callback", false, }, - { - "unexpected credentials", - "https://example.com/callback", - "https://user:pass@example.com/callback", - false, - }, { "wildcard password", "https://user:*@example.com/callback", @@ -347,7 +525,7 @@ func TestMatchCallbackURL(t *testing.T) { "backslash instead of forward slash", "https://example.com/callback", "https://example.com\\callback", - false, + true, }, { "double slash in hostname (protocol smuggling)", @@ -370,10 +548,11 @@ func TestMatchCallbackURL(t *testing.T) { } for _, tt := range tests { - matches, err := matchCallbackURL(tt.pattern, tt.input) - require.NoError(t, err, tt.name) - assert.Equal(t, tt.shouldMatch, matches, tt.name) - + t.Run(tt.name, func(t *testing.T) { + matches, err := matchCallbackURL(tt.pattern, tt.input) + require.NoError(t, err) + assert.Equal(t, tt.shouldMatch, matches) + }) } } @@ -407,14 +586,21 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) { expectMatch: true, }, { - name: "IPv6 loopback with dynamic port", + name: "IPv6 loopback with dynamic port - exact match", urls: []string{"http://[::1]/callback"}, inputCallbackURL: "http://[::1]:8080/callback", expectedURL: "http://[::1]:8080/callback", expectMatch: true, }, { - name: "IPv6 loopback with wildcard path", + name: "IPv6 loopback with same port - exact match", + urls: []string{"http://[::1]:8080/callback"}, + inputCallbackURL: "http://[::1]:8080/callback", + expectedURL: "http://[::1]:8080/callback", + expectMatch: true, + }, + { + name: "IPv6 loopback with path match", urls: []string{"http://[::1]/auth/*"}, inputCallbackURL: "http://[::1]:8080/auth/callback", expectedURL: "http://[::1]:8080/auth/callback", @@ -441,6 +627,20 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) { expectedURL: "http://127.0.0.1:3000/auth/callback", expectMatch: true, }, + { + name: "loopback with path port", + urls: []string{"http://127.0.0.1:*/auth/callback"}, + inputCallbackURL: "http://127.0.0.1:3000/auth/callback", + expectedURL: "http://127.0.0.1:3000/auth/callback", + expectMatch: true, + }, + { + name: "IPv6 loopback with path port", + urls: []string{"http://[::1]:*/auth/callback"}, + inputCallbackURL: "http://[::1]:3000/auth/callback", + expectedURL: "http://[::1]:3000/auth/callback", + expectMatch: true, + }, { name: "loopback with path mismatch", urls: []string{"http://127.0.0.1/callback"}, @@ -484,6 +684,76 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) { } } +func TestLoopbackURLWithWildcardPort(t *testing.T) { + tests := []struct { + name string + input string + output string + }{ + { + name: "localhost http with port strips port", + input: "http://localhost:3000/callback", + output: "http://localhost/callback", + }, + { + name: "localhost http without port stays same", + input: "http://localhost/callback", + output: "http://localhost/callback", + }, + { + name: "IPv4 loopback with port strips port", + input: "http://127.0.0.1:8080/callback", + output: "http://127.0.0.1/callback", + }, + { + name: "IPv4 loopback without port stays same", + input: "http://127.0.0.1/callback", + output: "http://127.0.0.1/callback", + }, + { + name: "IPv6 loopback with port strips port and keeps brackets", + input: "http://[::1]:8080/callback", + output: "http://[::1]/callback", + }, + { + name: "IPv6 loopback preserves path query and fragment", + input: "http://[::1]:8080/auth/callback?code=123#state", + output: "http://[::1]/auth/callback?code=123#state", + }, + { + name: "https loopback returns empty", + input: "https://127.0.0.1:8080/callback", + output: "", + }, + { + name: "non loopback host returns empty", + input: "http://example.com:8080/callback", + output: "", + }, + { + name: "non loopback IP returns empty", + input: "http://192.168.1.10:8080/callback", + output: "", + }, + { + name: "malformed URL returns empty", + input: "http://[::1:8080/callback", + output: "", + }, + { + name: "relative URL returns empty", + input: "/callback", + output: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.output, loopbackURLWithWildcardPort(tt.input)) + }) + } +} + func TestGetCallbackURLFromList_MultiplePatterns(t *testing.T) { tests := []struct { name string @@ -553,246 +823,3 @@ func TestGetCallbackURLFromList_MultiplePatterns(t *testing.T) { }) } } - -func TestMatchPath(t *testing.T) { - tests := []struct { - name string - pattern string - input string - shouldMatch bool - }{ - // Exact matches - { - name: "exact match", - pattern: "/callback", - input: "/callback", - shouldMatch: true, - }, - { - name: "exact mismatch", - pattern: "/callback", - input: "/other", - shouldMatch: false, - }, - { - name: "empty paths", - pattern: "", - input: "", - shouldMatch: true, - }, - - // Single wildcard (*) - { - name: "single wildcard matches segment", - pattern: "/api/*/callback", - input: "/api/v1/callback", - shouldMatch: true, - }, - { - name: "single wildcard doesn't match multiple segments", - pattern: "/api/*/callback", - input: "/api/v1/v2/callback", - shouldMatch: false, - }, - { - name: "single wildcard at end", - pattern: "/callback/*", - input: "/callback/test", - shouldMatch: true, - }, - { - name: "single wildcard at start", - pattern: "/*/callback", - input: "/api/callback", - shouldMatch: true, - }, - { - name: "multiple single wildcards", - pattern: "/*/test/*", - input: "/api/test/callback", - shouldMatch: true, - }, - { - name: "partial wildcard prefix", - pattern: "/test*", - input: "/testing", - shouldMatch: true, - }, - { - name: "partial wildcard suffix", - pattern: "/*-callback", - input: "/oauth-callback", - shouldMatch: true, - }, - { - name: "partial wildcard middle", - pattern: "/api-*-v1", - input: "/api-internal-v1", - shouldMatch: true, - }, - - // Double wildcard (**) - { - name: "double wildcard matches multiple segments", - pattern: "/api/**/callback", - input: "/api/v1/v2/v3/callback", - shouldMatch: true, - }, - { - name: "double wildcard matches single segment", - pattern: "/api/**/callback", - input: "/api/v1/callback", - shouldMatch: true, - }, - { - name: "double wildcard doesn't match when pattern has extra slashes", - pattern: "/api/**/callback", - input: "/api/callback", - shouldMatch: false, - }, - { - name: "double wildcard at end", - pattern: "/api/**", - input: "/api/v1/v2/callback", - shouldMatch: true, - }, - { - name: "double wildcard in middle", - pattern: "/api/**/v2/**/callback", - input: "/api/v1/v2/v3/v4/callback", - shouldMatch: true, - }, - - // Complex patterns - { - name: "mix of single and double wildcards", - pattern: "/*/api/**/callback", - input: "/app/api/v1/v2/callback", - shouldMatch: true, - }, - { - name: "wildcard with special characters", - pattern: "/callback-*", - input: "/callback-123", - shouldMatch: true, - }, - { - name: "path with query-like string (no special handling)", - pattern: "/callback?code=*", - input: "/callback?code=abc", - shouldMatch: true, - }, - - // Edge cases - { - name: "single wildcard matches empty segment", - pattern: "/api/*/callback", - input: "/api//callback", - shouldMatch: true, - }, - { - name: "pattern longer than input", - pattern: "/api/v1/callback", - input: "/api", - shouldMatch: false, - }, - { - name: "input longer than pattern", - pattern: "/api", - input: "/api/v1/callback", - shouldMatch: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - matches, err := matchPath(tt.pattern, tt.input) - require.NoError(t, err) - assert.Equal(t, tt.shouldMatch, matches) - }) - } -} - -func TestSplitParts(t *testing.T) { - tests := []struct { - name string - input string - expectedParts []string - expectedPath string - }{ - { - name: "simple https URL", - input: "https://example.com/callback", - expectedParts: []string{"https", "example", "com"}, - expectedPath: "/callback", - }, - { - name: "URL with port", - input: "https://example.com:8080/callback", - expectedParts: []string{"https", "example", "com", "8080"}, - expectedPath: "/callback", - }, - { - name: "URL with subdomain", - input: "https://api.example.com/callback", - expectedParts: []string{"https", "api", "example", "com"}, - expectedPath: "/callback", - }, - { - name: "URL with credentials", - input: "https://user:pass@example.com/callback", - expectedParts: []string{"https", "user", "pass", "example", "com"}, - expectedPath: "/callback", - }, - { - name: "URL without path", - input: "https://example.com", - expectedParts: []string{"https", "example", "com"}, - expectedPath: "", - }, - { - name: "URL with deep path", - input: "https://example.com/api/v1/callback", - expectedParts: []string{"https", "example", "com"}, - expectedPath: "/api/v1/callback", - }, - { - name: "URL with path and query", - input: "https://example.com/callback?code=123", - expectedParts: []string{"https", "example", "com"}, - expectedPath: "/callback?code=123", - }, - { - name: "URL with trailing slash", - input: "https://example.com/", - expectedParts: []string{"https", "example", "com"}, - expectedPath: "/", - }, - { - name: "URL with multiple subdomains", - input: "https://api.v1.staging.example.com/callback", - expectedParts: []string{"https", "api", "v1", "staging", "example", "com"}, - expectedPath: "/callback", - }, - { - name: "URL with port and credentials", - input: "https://user:pass@example.com:8080/callback", - expectedParts: []string{"https", "user", "pass", "example", "com", "8080"}, - expectedPath: "/callback", - }, - { - name: "scheme with authority separator but no slash", - input: "http://example.com", - expectedParts: []string{"http", "example", "com"}, - expectedPath: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parts, path := splitParts(tt.input) - assert.Equal(t, tt.expectedParts, parts, "parts mismatch") - assert.Equal(t, tt.expectedPath, path, "path mismatch") - }) - } -} diff --git a/backend/resources/aaguids.json b/backend/resources/aaguids.json index 2bead1a3..f6ead537 100644 --- a/backend/resources/aaguids.json +++ b/backend/resources/aaguids.json @@ -1 +1 @@ -{"fcb1bcb4-f370-078c-6993-bc24d0ae3fbe":"Ledger Nano X FIDO2 Authenticator","6e8d1eae-8d40-4c25-bcf8-4633959afc71":"Veridium iOS SDK","9eb7eabc-9db5-49a1-b6c3-555a802093f4":"YubiKey 5 Series with NFC KVZR57","4d41190c-7beb-4a84-8018-adf265a6352d":"Thales IDPrime FIDO Bio","2772ce93-eb4b-4090-8b73-330f48477d73":"Security Key NFC by Yubico - Enterprise Edition Preview","6dae43be-af9c-417b-8b9f-1b611168ec60":"Dapple Authenticator from Dapple Security Inc.","5626bed4-e756-430b-a7ff-ca78c8b12738":"VALMIDO PRO FIDO","260e3021-482d-442d-838c-7edfbe153b7e":"Feitian ePass FIDO2-NFC Plus Authenticator","95e4d58c-056e-4a65-866d-f5a69659e880":"TruU Windows Authenticator","90636e1f-ef82-43bf-bdcf-5255f139d12f":"YubiKey Bio Series - Multi-protocol Edition","9c835346-796b-4c27-8898-d6032f515cc5":"Cryptnox FIDO2","c3f47802-de73-4dfc-ba22-671fe3304f90":"eToken Fusion NFC PIV Enterprise","0d9b2e56-566b-c393-2940-f821b7f15d6d":"Excelsecu eSecu FIDO2 Pro Security Key","2bff89f2-323a-48fc-b7c8-9ff7fe87c07e":"Feitian BioPass FIDO2 Pro (Enterprise Profile)","c5ef55ff-ad9a-4b9f-b580-adebafe026d0":"YubiKey 5 Series with Lightning","2194b428-9397-4046-8f39-007a1605a482":"IDPrime 931 Fido","39a5647e-1853-446c-a1f6-a79bae9f5bc7":"IDmelon","664d9f67-84a2-412a-9ff7-b4f7d8ee6d05":"OpenSK authenticator","3789da91-f943-46bc-95c3-50ea2012f03a":"NEOWAVE Winkeo FIDO2","fa2b99dc-9e39-4257-8f92-4a30d23c4118":"YubiKey 5 Series with NFC","341e4da9-3c2e-8103-5a9f-aad887135200":"Ledger Nano S FIDO2 Authenticator","69700f79-d1fb-472e-bd9b-a3a3b9a9eda0":"Pone Biometrics OFFPAD Authenticator","8da0e4dc-164b-454e-972e-88f362b23d59":"CardOS FIDO2 Token","89b19028-256b-4025-8872-255358d950e4":"Sentry Enterprises CTAP2 Authenticator","4e768f2c-5fab-48b3-b300-220eb487752b":"Hideez Key 4 FIDO2 SDK","47ab2fb4-66ac-4184-9ae1-86be814012d5":"Security Key NFC by Yubico - Enterprise Edition","931327dd-c89b-406c-a81e-ed7058ef36c6":"Swissbit iShield Key FIDO2","f8d5c4e9-e539-4c06-8662-ec2a4155a555":"StarSign Key Fob","b7d3f68e-88a6-471e-9ecf-2df26d041ede":"Security Key NFC by Yubico","8d1b1fcb-3c76-49a9-9129-5515b346aa02":"IDEMIA ID-ONE Card","30b5035e-d297-4ff7-020b-addc96ba6a98":"OneSpan DIGIPASS FX7","454e5346-4944-4ffd-6c93-8e9267193e9a":"Ensurity ThinC","e1a96183-5016-4f24-b55b-e3ae23614cc6":"ATKey.Pro CTAP2.0","9ff4cc65-6154-4fff-ba09-9e2af7882ad2":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","4599062e-6926-4fe7-9566-9e8fb1aedaa0":"YubiKey 5 Series (Enterprise Profile)","9d3df6ba-282f-11ed-a261-0242ac120002":"Arculus FIDO2/U2F Key Card","fbefdf68-fe86-0106-213e-4d5fa24cbe2e":"Excelsecu eSecu FIDO2 NFC Security Key","62e54e98-c209-4df3-b692-de71bb6a8528":"YubiKey 5 FIPS Series with NFC Preview","ab32f0c6-2239-afbb-c470-d2ef4e254db7":"TOKEN2 FIDO2 Security Key","ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1":"YubiKey 5 FIPS Series with NFC (RC Preview)","ad08c78a-4e41-49b9-86a2-ac15b06899e2":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","930b0c03-ef46-4ac4-935c-538dccd1fcdb":"Chipwon Clife Key","7787a482-13e8-4784-8a06-c7ed49a7aaf4":"Swissbit iShield Key 2","72c6b72d-8512-4c66-8359-9d3d10d9222f":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","99ed6c29-4573-4847-816d-78ad8f1c75ef":"VeroCard FIDO2 Authenticator","973446ca-e21c-9a9b-99f5-9b985a67af0f":"ACS FIDO Authenticator Card","74820b05-a6c9-40f9-8fb0-9f86aca93998":"SafeNet eToken Fusion","1105e4ed-af1d-02ff-ffff-ffffffffffff":"Egomet FIDO2 Authenticator for Android","08987058-cadc-4b81-b6e1-30de50dcbe96":"Windows Hello","a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa":"Security Key NFC by Yubico","0acf3011-bc60-f375-fb53-6f05f43154e0":"Nymi FIDO2 Authenticator","d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":"KEY-ID FIDO2 Authenticator","8eec9bf9-486c-46da-9a67-1fbb4f66b9ed":"HID Crescendo 4000 FIPS","4c50ff10-1057-4fc6-b8ed-43a529530c3c":"ImproveID Authenticator","c611b55c-77b2-4527-8082-590e931b2f08":"GoTrust Idem Key (Consumer profile)","ee041bce-25e5-4cdb-8f86-897fd6418464":"Feitian ePass FIDO2-NFC Authenticator","4b89f401-464e-4745-a520-486ddfc5d80e":"IIST FIDO2 Authenticator","2cd2f727-f6ca-44da-8f48-5c2e5da000a2":"Nitrokey 3 AM","10c70715-2a9a-4de1-b0aa-3cff6d496d39":"eToken Fusion NFC FIPS","efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4":"Safenet eToken FIDO","4b3f8944-d4f2-4d21-bb19-764a986ec160":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","4c0cf95d-2f40-43b5-ba42-4c83a11c04ba":"Feitian BioPass FIDO2 Pro Authenticator","5343502d-5343-5343-6172-644649444f32":"ESS Smart Card Inc. Authenticator","69e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Authenticator","09591fc6-9811-48f7-8f57-b9f23df6413f":"Pone Biometrics OFFPAD Authenticator","912435d9-4a88-42f3-972d-1244b0d51420":"SI0X FIDO CL WRIST v1.0","7e3f3d30-3557-4442-bdae-139312178b39":"RSA DS100","73bb0cd4-e502-49b8-9c6f-b59445bf720b":"YubiKey 5 FIPS Series","39589099-9a75-49fc-afaa-801ca211c62a":"Feitian ePass FIDO-NFC (Enterprise Profile) (CTAP2.1, CTAP2.0, U2F)","149a2021-8ef6-4133-96b8-81f8d5b7f1f5":"Security Key by Yubico with NFC","5df66f62-5b47-43d3-aa1d-a6e31c8dbeb5":"Securitag Assembly Group FIDO Authenticator NFC","09619fbf-d75e-4a62-be1d-fe4d240864ae":"VeriMark(TM) Guard 2.1 Fingerprint Security Key","50cbf15a-238c-4457-8f16-812c43bf3c49":"Ensurity AUTH TouchPro","ee7fa1e0-9539-432f-bd43-9c2fc6d4f311":"VeriMark NFC+ USB-C Security Key","b90e7dc1-316e-4fee-a25a-56a666a670fe":"YubiKey 5 Series with Lightning (Enterprise Profile)","175cd298-83d2-4a26-b637-313c07a6434e":"Chunghwa Telecom FIDO2 Smart Card Authenticator","34744913-4f57-4e6e-a527-e9ec3c4b94e6":"YubiKey Bio Series - Multi-protocol Edition","5ea308b2-7ac7-48b9-ac09-7e2da9015f8c":"Veridium Android SDK","3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":"GoTrust Idem Key FIDO2 Authenticator","46544d5d-8f5d-4db4-89ac-ea8977073fff":"Foongtone FIDO Authenticator","998f358b-2dd2-4cbe-a43a-e8107438dfb3":"OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator","30b5035e-d297-4ff2-010b-addc96ba6a98":"OneSpan DIGIPASS FX2-A","817cdab8-0d51-4de1-a821-e25b88519cf3":"Swissbit iShield Key 2 FIPS","61250591-b2bc-4456-b719-0b17be90bb30":"eWBM eFPA FIDO2 Authenticator","8c39ee86-7f9a-4a95-9ba3-f6b097e5c2ee":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","f8a011f3-8c0a-4d15-8006-17111f9edc7d":"Security Key by Yubico","8976631b-d4a0-427f-5773-0ec71c9e0279":"Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator","516d3969-5a57-5651-5958-4e7a49434167":"SmartDisplayer BobeePass FIDO2 Authenticator","8681a073-5f50-4d52-bce4-e21658d207b3":"RSA Authenticator 4 for iOS","30b5035e-d297-4ff7-030b-addc96ba6a98":"OneSpan DIGIPASS FX7-C","e41b42a3-60ac-4afb-8757-a98f2d7f6c9f":"Deepnet SafeKey/Classic (FP)","c89e6a38-6c00-5426-5aa5-c9cbf48f0382":"ACS FIDO Authenticator NFC","a02167b9-ae71-4ac7-9a07-06432ebb6f1c":"YubiKey 5 Series with Lightning","82b0a720-127a-4788-b56d-d1d4b2d82eac":"ID-One Key","2c0df832-92de-4be1-8412-88a8f074df4a":"Feitian FIDO Smart Card","59f85fe7-faa5-4c92-9f52-697b9d4d5473":"RSA Authenticator 4 for Android","79f3c8ba-9e35-484b-8f47-53a5a0f5c630":"YubiKey 5 FIPS Series with NFC (Enterprise Profile)","def8ab1a-9f91-44f1-a103-088d8dc7d681":"IDEMIA SOLVO Fly 80 R3 FIDO Card e","970c8d9c-19d2-46af-aa32-3f448db49e35":"WinMagic FIDO Eazy - TPM","c5703116-972b-4851-a3e7-ae1259843399":"NEOWAVE Badgeo FIDO2","c80dbd9a-533f-4a17-b941-1a2f1c7cedff":"HID Crescendo C3000","5b0e46ba-db02-44ac-b979-ca9b84f5e335":"YubiKey 5 FIPS Series with Lightning Preview","12755c32-8ad1-46eb-881c-e0b38d848b09":"Feitian ePass FIDO Authenticator (CTAP2.1, CTAP2.0, U2F)","2a55aee6-27cb-42c0-bc6e-04efe999e88a":"HID Crescendo 4000","820d89ed-d65a-409e-85cb-f73f0578f82a":"IDmelon iOS Authenticator","019614a3-2703-7e35-a453-285fd06c5d24":"ATLKey Authenticator","3124e301-f14e-4e38-876d-fbeeb090e7bf":"YubiKey 5 Series with Lightning Preview","b6ede29c-3772-412c-8a78-539c1f4c62d2":"Feitian BioPass FIDO2 Plus Authenticator","ed042a3a-4b22-4455-bb69-a267b652ae7e":"Security Key NFC by Yubico - Enterprise Edition","b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc":"YubiKey 5 Series with NFC - Enhanced PIN (Enterprise Profile)","85203421-48f9-4355-9bc8-8a53846e5083":"YubiKey 5 FIPS Series with Lightning","fcc0118f-cd45-435b-8da1-9782b2da0715":"YubiKey 5 FIPS Series with NFC","d821a7d4-e97c-4cb6-bd82-4237731fd4be":"Hyper FIDO Bio Security Key","9876631b-d4a0-427f-5773-0ec71c9e0279":"Somu Secp256R1 FIDO2 CTAP2 Authenticator","f56f58b3-d711-4afc-ba7d-6ac05f88cb19":"WinMagic FIDO Eazy - Phone","6ec5cff2-a0f9-4169-945b-f33b563f7b99":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","882adaf5-3aa9-4708-8e7d-3957103775b4":"T-Shield TrustSec FIDO2 Bio and client PIN version","49a15c1c-3f63-3f51-23a7-b9e00096edd1":"IDEX CTAP2.1 Biometrics","f4c63eff-d26c-4248-801c-3736c7eaa93a":"FIDO KeyPass S3","d384db22-4d50-ebde-2eac-5765cf1e2a44":"Excelsecu eSecu FIDO2 Fingerprint Security Key","0db01cd6-5618-455b-bb46-1ec203d3213e":"GoldKey Security Token","b93fd961-f2e6-462f-b122-82002247de78":"Android Authenticator","aa79f476-ea00-417e-9628-1e8365123922":"HID Crescendo 4000 FIDO","1e906e14-77af-46bc-ae9f-fe6ef18257e4":"VeridiumID Passkey iOS SDK","2fc0579f-8113-47ea-b116-bb5a8db9202a":"YubiKey 5 Series with NFC","31c3f7ff-bf15-4327-83ec-9336abcbcd34":"WinMagic FIDO Eazy - Software","9ddd1817-af5a-4672-a2b9-3e3dd95000a9":"Windows Hello","d8522d9f-575b-4866-88a9-ba99fa02f35b":"YubiKey Bio Series - FIDO Edition","050dd0bc-ff20-4265-8d5d-305c4b215192":"eToken Fusion FIPS","50a45b0c-80e7-f944-bf29-f552bfa2e048":"ACS FIDO Authenticator","f7c558a0-f465-11e8-b568-0800200c9a66":"KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator","3f59672f-20aa-4afe-b6f4-7e5e916b6d98":"Arculus FIDO 2.1 Key Card [P71]","42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3":"Google Titan Security Key v2","361a3082-0278-4583-a16f-72a527f973e4":"eWBM eFA500 FIDO2 Authenticator","2ffd6452-01da-471f-821b-ea4bf6c8676a":"IDPrime 941 Fido","30b5035e-d297-4ff7-b00b-addc96ba6a98":"OneSpan DIGIPASS FX7","5eaff75a-dd43-451f-af9f-87c9eeae293e":"Swissbit iShield Key 2 FIPS Enterprise","b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4":"ZTPass SmartAuth","692db549-7ae5-44d5-a1e5-dd20a493b723":"HID Crescendo Key","23315ad0-6aca-4ba1-952e-f044f1e36976":"Clife Key 2 NFC","1d1b4e33-76a1-47fb-97a0-14b10d0933f1":"Cryptnox FIDO2.1","bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a":"Excelsecu eSecu FIDO2 PRO Security Key","3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":"Feitian iePass FIDO Authenticator","23786452-f02d-4344-87ed-aaf703726881":"SafeNet eToken Fusion CC","5e264d9d-28ef-4d34-95b4-5941e7a4faa8":"Ideem ZSM FIDO2 Authenticator","d2fbd093-ee62-488d-9dad-1e36389f8826":"YubiKey 5 FIPS Series (RC Preview)","234cd403-35a2-4cc2-8015-77ea280c77f5":"Feitian ePass FIDO2-NFC Series (CTAP2.1, CTAP2.0, U2F)","6999180d-630c-442d-b8f7-424b90a43fae":"Hyper FIDO Pro (CTAP2.1, CTAP2.0, U2F)","662ef48a-95e2-4aaa-a6c1-5b9c40375824":"YubiKey 5 Series with NFC - Enhanced PIN","aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":"HID Crescendo C2300","87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c":"eWBM eFA320 FIDO2 Authenticator","58276709-bb4b-4bb3-baf1-60eea99282a7":"YubiKey Bio Series - Multi-protocol Edition 1VDJSN","7d2afadd-bf6b-44a2-a66b-e831fceb8eff":"Taglio CTAP2.1 EP","30b5035e-d297-4ff1-020b-addc96ba6a98":"OneSpan DIGIPASS FX1-C","20ac7a17-c814-4833-93fe-539f0d5e3389":"YubiKey 5 Series (Enterprise Profile)","9012593f-43e4-4461-a97a-d92777b55d74":"VinCSS FIDO2 Fingerprint","d7781e5d-e353-46aa-afe2-3ca49f13332a":"YubiKey 5 Series with NFC","9f0d8150-baa5-4c00-9299-ad62c8bb4e87":"GoTrust Idem Card FIDO2 Authenticator","12ded745-4bed-47d4-abaa-e713f51d6393":"Feitian AllinOne FIDO2 Authenticator","88bbd2f0-342a-42e7-9729-dd158be5407a":"Precision InnaIT Key FIDO 2 Level 2 certified","1d8cac46-47a1-3386-af50-e88ae46fe802":"Ledger Flex FIDO2 Authenticator","dd86a2da-86a0-4cbe-b462-4bd31f57bc6f":"YubiKey Bio Series - FIDO Edition","773c30d9-5919-4e96-a4f5-db65e95cf890":"GSTAG OAK FIDO2 Authenticator","34f5766d-1536-4a24-9033-0e294e510fb0":"YubiKey 5 Series with NFC Preview","83c47309-aabb-4108-8470-8be838b573cb":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","4e2ddbc2-2687-4709-8551-cb66c9776bfe":"SECORA ID V2 FIDO2.1 L1","be727034-574a-f799-5c76-0929e0430973":"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)","092277e5-8437-46b5-b911-ea64b294acb7":"Taglio CTAP2.1 CS","ca87cb70-4c1b-4579-a8e8-4efdd7c007e0":"FIDO Alliance TruU Sample FIDO2 Authenticator","23195a52-62d9-40fa-8ee5-23b173f4fb52":"Hyper FIDO Pro NFC","a7fc3f84-86a3-4da4-a3d7-eb6485a066d8":"NEOWAVE Badgeo FIDO2 (CTAP 2.1)","9e66c661-e428-452a-a8fb-51f7ed088acf":"YubiKey 5 FIPS Series with Lightning (RC Preview)","58b44d0b-0a7c-f33a-fd48-f7153c871352":"Ledger Nano S Plus FIDO2 Authenticator","454e5346-4944-4ffd-6c93-8e9267193e9b":"Ensurity AUTH BioPro","146e77ef-11eb-4423-b847-ce77864e9411":"eToken Fusion NFC PIV","13ac47cf-1d78-4fd5-9060-aedaabacf826":"HID Crescendo Key V3 - Enterprise Edition","e77e3c64-05e3-428b-8824-0cbeb04b829d":"Security Key NFC by Yubico","33d6d7d0-279f-4ef3-96b3-2d3282f4bde6":"Thales eToken Fusion BIO Enterprise","8d4378b0-725d-4432-b3c2-01fcdaf46286":"VeridiumID Passkey Android SDK","7409272d-1ff9-4e10-9fc9-ac0019c124fd":"YubiKey Bio Series - FIDO Edition","bb66c294-de08-47e4-b7aa-d12c2cd3fb20":"Mettlesemi Vishwaas Hawk Authenticator using FIDO2","c4ddaf11-3032-4e77-b3b9-3a340369b9ad":"HID Crescendo Fusion","7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3":"YubiKey Bio Series - Multi-protocol Edition","07a9f89c-6407-4594-9d56-621d5f1e358b":"NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator","d61d3b87-3e7c-4aea-9c50-441c371903ad":"KeyVault Secp256R1 FIDO2 CTAP2 Authenticator","c62100de-759b-4bf8-b22b-63b3e3a80401":"Token Ring 3 FIDO2 Authenticator","5ca1ab1e-1337-fa57-f1d0-a117e71ca702":"Allthenticator iOS App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","b92c3f9a-c014-4056-887f-140a2501163b":"Security Key by Yubico","54d9fee8-e621-4291-8b18-7157b99c5bec":"HID Crescendo Enabled","a25342c0-3cdc-4414-8e46-f4807fca511c":"YubiKey 5 Series with NFC","3a662962-c6d4-4023-bebb-98ae92e78e20":"YubiKey 5 FIPS Series with Lightning (Enterprise Profile)","20f0be98-9af9-986a-4b42-8eca4acb28e4":"Excelsecu eSecu FIDO2 Fingerprint Security Key","ca4cff1b-5a81-4404-8194-59aabcf1660b":"IDPrime 3930 FIDO","ab32f0c6-2239-afbb-c470-d2ef4e254db6":"TEST (DUMMY RECORD)","760eda36-00aa-4d29-855b-4012a182cdeb":"Security Key NFC by Yubico Preview","6028b017-b1d4-4c02-b4b3-afcdafc96bb2":"Windows Hello","b12eac35-586c-4809-a4b1-d81af6c305cf":"Deepnet SafeKey/Classic (NFC)","30b5035e-d297-4fc1-b00b-addc96ba6a97":"OneSpan FIDO Touch","560a780c-b6ae-4f03-b110-082f856425b4":"KQC QuKey Bio FIDO2 Authenticator","1ac71f64-468d-4fe0-bef1-0e5f2f551f18":"YubiKey 5 Series with NFC (Enterprise Profile)","6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":"Security Key by Yubico with NFC","9eb85bb6-9625-4a72-815d-0487830ccab2":"Ensurity AUTH BioPro Desktop","30b5035e-d297-4ff7-010b-addc96ba6a98":"OneSpan DIGIPASS FX7-B","5ca1ab1e-fa57-1337-f1d0-a117371ca702":"Allthenticator Android App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","eabb46cc-e241-80bf-ae9e-96fa6d2975cf":"TOKEN2 PIN Plus Security Key Series ","53414d53-554e-4700-0000-000000000000":"Samsung Pass","e416201b-afeb-41ca-a03d-2281c28322aa":"ATKey.Pro CTAP2.1","905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7":"YubiKey 5 FIPS Series (Enterprise Profile)","cfcb13a2-244f-4b36-9077-82b79d6a7de7":"USB/NFC Passcode Authenticator","76692dc1-c56a-48d9-8e7d-31b5ced430ac":"VeriMark NFC+ USB-A Security Key","91ad6b93-264b-4987-8737-3a690cad6917":"Token Ring FIDO2 Authenticator","a02140b7-0cbd-42e1-a9b5-a39da2545114":"Feitian BioPass FIDO2 Plus (Enterprise Profile)","5753362b-4e6b-6345-7b2f-255438404c75":"WiSECURE Blentity FIDO2 Authenticator","9f77e279-a6e2-4d58-b700-31e5943c6a98":"Hyper FIDO Pro","b9f6b7b6-f929-4189-bca9-dd951240c132":"Deepnet SafeKey/Classic (USB)","cc45f64e-52a2-451b-831a-4edd8022a202":"ToothPic Passkey Provider","0bb43545-fd2c-4185-87dd-feb0b2916ace":"Security Key NFC by Yubico - Enterprise Edition","73402251-f2a8-4f03-873e-3cb6db604b03":"uTrust FIDO2 Security Key","c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":"YubiKey 5 FIPS Series with NFC","70e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Bluetooth Authenticator","4fc84f16-2545-4e53-b8fc-7bf4d7282a10":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","6ab56fad-881f-4a43-acb2-0be065924522":"YubiKey 5 Series with NFC (Enterprise Profile)","504d7149-4e4c-3841-4555-55445a677357":"WiSECURE AuthTron USB FIDO2 Authenticator","2c2aeed8-8174-4159-814b-486e92a261d0":"NEOWAVE WINKEO V2.0","f2145e86-211e-4931-b874-e22bba7d01cc":"ID-One Key","a3975549-b191-fd67-b8fb-017e2917fdb3":"Excelsecu eSecu FIDO2 NFC Security Key","19083c3d-8383-4b18-bc03-8f1c9ab2fd1b":"YubiKey 5 Series","da1fa263-8b25-42b6-a820-c0036f21ba7f":"ATKey.Card NFC","6002f033-3c07-ce3e-d0f7-0ffe5ed42543":"Excelsecu eSecu FIDO2 Fingerprint Key","5fdb81b8-53f0-4967-a881-f5ec26fe4d18":"VinCSS FIDO2 Authenticator","78ba3993-d784-4f44-8d6e-cc0a8ad5230e":"Feitian ePass FIDO-NFC(CTAP2.1, CTAP2.0, U2F)","57f7de54-c807-4eab-b1c6-1c9be7984e92":"YubiKey 5 FIPS Series","bb405265-40cf-4115-93e5-a332c1968d8c":"ID-One Card","2d3bec26-15ee-4f5d-88b2-53622490270b":"HID Crescendo Key V2","489ff376-b48d-6640-bb69-782a860ca795":"Mettlesemi Vishwaas Eagle Authenticator using FIDO2","3b24bf49-1d45-4484-a917-13175df0867b":"YubiKey 5 Series with Lightning (Enterprise Profile)","30b5035e-d297-4ff1-010b-addc96ba6a98":"OneSpan DIGIPASS FX1a","cb69481e-8ff7-4039-93ec-0a2729a154a8":"YubiKey 5 Series","0076631b-d4a0-427f-5773-0ec71c9e0279":"HYPR FIDO2 Authenticator","d716019a-9f4e-4041-9750-17c78f8ae81a":"eToken Fusion BIO","57235694-51a5-4a4d-a81a-f42185df6502":"SHALO AUTH","24673149-6c86-42e7-98d9-433fb5b73296":"YubiKey 5 Series with Lightning","42df17de-06ba-4177-a2bb-6701be1380d6":"Feitian BioPass FIDO2 Plus Authenticator","d7a423ad-3e19-4492-9200-78137dccc136":"VivoKey Apex FIDO2","ba76a271-6eb6-4171-874d-b6428dbe3437":"ATKey.ProS","97e6a830-c952-4740-95fc-7c78dc97ce47":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","f573f209-b7fb-b261-671a-d7cf624cc812":"Excelsecu eSecu FIDO2 PRO+ Security Key","6e24d385-004a-16a0-7bfe-efd963845b34":"Ledger Stax FIDO2 Authenticator","ee882879-721c-4913-9775-3dfcce97072a":"YubiKey 5 Series","8876631b-d4a0-427f-5773-0ec71c9e0279":"Solo Secp256R1 FIDO2 CTAP2 Authenticator","fec067a1-f1d0-4c5e-b4c0-cc3237475461":"KX701 SmartToken FIDO","30b5035e-d297-4ff1-b00b-addc96ba6a98":"OneSpan DIGIPASS FX1 BIO","b267239b-954f-4041-a01b-ee4f33c145b6":"authenton1 - CTAP2.1","b50d5e0a-7f81-4959-9b12-f45407407503":"IDPrime 3940 FIDO","8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":"FT-JCOS FIDO Fingerprint Card","99bf4610-ec26-4252-b31f-7380ccd59db5":"ZTPass SmartAuth","a1f52be5-dfab-4364-b51c-2bd496b14a56":"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR","0f00cc22-4640-41e7-9585-384ec73ffe9b":"Taglio CTAP2.1 BIO","ff4dac45-ede8-4ec2-aced-cf66103f4335":"YubiKey 5 Series","ba86dc56-635f-4141-aef6-00227b1b9af6":"TruU Windows Authenticator","3e078ffd-4c54-4586-8baa-a77da113aec5":"Hideez Key 3 FIDO2","fc5ca237-69a0-4f3c-afe4-1ebc66def6df":"Clife Key 2","ec31b4cc-2acc-4b8e-9c01-bade00ccbe26":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","5d629218-d3a5-11ed-afa1-0242ac120002":"Swissbit iShield Key Pro","bb878d7b-cf54-4784-b390-357030497043":"TruU FIDO2 Authenticator","d41f5a69-b817-4144-a13c-9ebd6d9254d6":"ATKey.Card CTAP2.0","e86addcd-7711-47e5-b42a-c18257b0bf61":"IDCore 3121 Fido","b113a455-cfb6-4c17-8cba-cd952feb7d48":"eToken FIDO NFC","95442b2e-f15e-4def-b270-efb106facb4e":"eWBM eFA310 FIDO2 Authenticator","dda9aa35-aaf1-4d3c-b6db-7902fd7dbbbf":"IDEMIA SOLVO Fly 80 R3 FIDO Card c","cdbdaea2-c415-5073-50f7-c04e968640b6":"Excelsecu eSecu FIDO2 Security Key","3aa78eb1-ddd8-46a8-a821-8f8ec57a7bd5":"YubiKey 5 CCN Series with NFC","bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":"OCTATCO EzQuant FIDO2 AUTHENTICATOR","eb3b131e-59dc-536a-d176-cb7306da10f5":"ellipticSecure MIRkey USB Authenticator","3fd410dc-8ab7-4b86-a1cb-c7174620b2dc":"IDEMIA SOLVO Fly 80 R1 FIDO Card Draft","a6c5f5d8-2ad0-48b6-8257-e502c8970931":"eToken FIDO NFC Enterprise","e400ef8c-711d-4692-af46-7f2cf7da23ad":"Swissbit iShield Key 2 Enterprise","1c086528-58d5-f211-823c-356786e36140":"Atos CardOS FIDO2","77010bd7-212a-4fc9-b236-d2ca5e9d4084":"Feitian BioPass FIDO2 Authenticator","d94a29d9-52dd-4247-9c2d-8b818b610389":"VeriMark Guard Fingerprint Key","7b96457d-e3cd-432b-9ceb-c9fdd7ef7432":"YubiKey 5 FIPS Series with Lightning","7991798a-a7f3-487f-98c0-3faf7a458a04":"HID Crescendo Key V3","833b721a-ff5f-4d00-bb2e-bdda3ec01e29":"Feitian ePass FIDO2 Authenticator","c89674e3-a765-4b07-888a-7c086fbdf04b":"StarSign FIDO Card","a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942":"AliasVault","ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4":"Google Password Manager","adce0002-35bc-c60a-648b-0b25f1f05503":"Chrome on Mac","dd4ec289-e01d-41c9-bb89-70fa845d4bf2":"iCloud Keychain (Managed)","531126d6-e717-415c-9320-3d9aa6981239":"Dashlane","bada5566-a7aa-401f-bd96-45619a55120d":"1Password","b84e4048-15dc-4dd0-8640-f4f60813c8af":"NordPass","0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6":"Keeper","891494da-2c90-4d31-a9cd-4eab0aed1309":"Sésame","f3809540-7f14-49c1-a8b3-8f813b225541":"Enpass","b5397666-4885-aa6b-cebf-e52262a439a2":"Chromium Browser","771b48fd-d3d4-4f74-9232-fc157ab0507a":"Edge on Mac","d548826e-79b4-db40-a3d8-11116f7e8349":"Bitwarden","fbfc3007-154e-4ecc-8c0b-6e020557d7bd":"Apple Passwords","66a0ccb3-bd6a-191f-ee06-e375c50b9846":"Thales Bio iOS SDK","8836336a-f590-0921-301d-46427531eee6":"Thales Bio Android SDK","cd69adb5-3c7a-deb9-3177-6800ea6cb72a":"Thales PIN Android SDK","17290f1e-c212-34d0-1423-365d729f09d9":"Thales PIN iOS SDK","50726f74-6f6e-5061-7373-50726f746f6e":"Proton Pass","fdb141b2-5d84-443e-8a35-4698c205a502":"KeePassXC","eaecdef2-1c31-5634-8639-f1cbd9c00a08":"KeePassDX","bfc748bb-3429-4faa-b9f9-7cfa9f3b76d0":"iPasswords","b35a26b2-8f6e-4697-ab1d-d44db4da28c6":"Zoho Vault","b78a0a55-6ef8-d246-a042-ba0f6d55050c":"LastPass","de503f9c-21a4-4f76-b4b7-558eb55c6f89":"Devolutions","22248c4c-7a12-46e2-9a41-44291b373a4d":"LogMeOnce","a10c6dd9-465e-4226-8198-c7c44b91c555":"Kaspersky Password Manager","d350af52-0351-4ba2-acd3-dfeeadc3f764":"pwSafe","d3452668-01fd-4c12-926c-83a4204853aa":"Microsoft Password Manager","6d212b28-a2c1-4638-b375-5932070f62e9":"initial","d49b2120-b865-4191-8cea-be84a52b0485":"Heimlane Vault","e8b7f4a2-c3d5-e6f7-890a-b1c2d3e4f567":"Sherlocked"} +{"fcb1bcb4-f370-078c-6993-bc24d0ae3fbe":"Ledger Nano X FIDO2 Authenticator","6e8d1eae-8d40-4c25-bcf8-4633959afc71":"Veridium iOS SDK","9eb7eabc-9db5-49a1-b6c3-555a802093f4":"YubiKey 5 Series with NFC KVZR57","4d41190c-7beb-4a84-8018-adf265a6352d":"Thales IDPrime FIDO Bio","2772ce93-eb4b-4090-8b73-330f48477d73":"Security Key NFC by Yubico - Enterprise Edition Preview","6dae43be-af9c-417b-8b9f-1b611168ec60":"Dapple Authenticator from Dapple Security Inc.","5626bed4-e756-430b-a7ff-ca78c8b12738":"VALMIDO PRO FIDO","260e3021-482d-442d-838c-7edfbe153b7e":"Feitian ePass FIDO2-NFC Plus Authenticator","95e4d58c-056e-4a65-866d-f5a69659e880":"TruU Windows Authenticator","90636e1f-ef82-43bf-bdcf-5255f139d12f":"YubiKey Bio Series - Multi-protocol Edition","9c835346-796b-4c27-8898-d6032f515cc5":"Cryptnox FIDO2","c3f47802-de73-4dfc-ba22-671fe3304f90":"eToken Fusion NFC PIV Enterprise","0d9b2e56-566b-c393-2940-f821b7f15d6d":"Excelsecu eSecu FIDO2 Pro Security Key","2bff89f2-323a-48fc-b7c8-9ff7fe87c07e":"Feitian BioPass FIDO2 Pro (Enterprise Profile)","c5ef55ff-ad9a-4b9f-b580-adebafe026d0":"YubiKey 5 Series with Lightning","2194b428-9397-4046-8f39-007a1605a482":"IDPrime 931 Fido","39a5647e-1853-446c-a1f6-a79bae9f5bc7":"IDmelon","664d9f67-84a2-412a-9ff7-b4f7d8ee6d05":"OpenSK authenticator","3789da91-f943-46bc-95c3-50ea2012f03a":"NEOWAVE Winkeo FIDO2","fa2b99dc-9e39-4257-8f92-4a30d23c4118":"YubiKey 5 Series with NFC","341e4da9-3c2e-8103-5a9f-aad887135200":"Ledger Nano S FIDO2 Authenticator","69700f79-d1fb-472e-bd9b-a3a3b9a9eda0":"Pone Biometrics OFFPAD Authenticator","8da0e4dc-164b-454e-972e-88f362b23d59":"CardOS FIDO2 Token","89b19028-256b-4025-8872-255358d950e4":"Sentry Enterprises CTAP2 Authenticator","4e768f2c-5fab-48b3-b300-220eb487752b":"Hideez Key 4 FIDO2 SDK","47ab2fb4-66ac-4184-9ae1-86be814012d5":"Security Key NFC by Yubico - Enterprise Edition","931327dd-c89b-406c-a81e-ed7058ef36c6":"Swissbit iShield Key FIDO2","f8d5c4e9-e539-4c06-8662-ec2a4155a555":"StarSign Key Fob","b7d3f68e-88a6-471e-9ecf-2df26d041ede":"Security Key NFC by Yubico","8d1b1fcb-3c76-49a9-9129-5515b346aa02":"IDEMIA ID-ONE Card","30b5035e-d297-4ff7-020b-addc96ba6a98":"OneSpan DIGIPASS FX7","454e5346-4944-4ffd-6c93-8e9267193e9a":"Ensurity ThinC","e1a96183-5016-4f24-b55b-e3ae23614cc6":"ATKey.Pro CTAP2.0","9ff4cc65-6154-4fff-ba09-9e2af7882ad2":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","4599062e-6926-4fe7-9566-9e8fb1aedaa0":"YubiKey 5 Series (Enterprise Profile)","9d3df6ba-282f-11ed-a261-0242ac120002":"Arculus FIDO2/U2F Key Card","fbefdf68-fe86-0106-213e-4d5fa24cbe2e":"Excelsecu eSecu FIDO2 NFC Security Key","62e54e98-c209-4df3-b692-de71bb6a8528":"YubiKey 5 FIPS Series with NFC Preview","ab32f0c6-2239-afbb-c470-d2ef4e254db7":"TOKEN2 FIDO2 Security Key","ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1":"YubiKey 5 FIPS Series with NFC (RC Preview)","ad08c78a-4e41-49b9-86a2-ac15b06899e2":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","930b0c03-ef46-4ac4-935c-538dccd1fcdb":"Chipwon Clife Key","7787a482-13e8-4784-8a06-c7ed49a7aaf4":"Swissbit iShield Key 2","72c6b72d-8512-4c66-8359-9d3d10d9222f":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","99ed6c29-4573-4847-816d-78ad8f1c75ef":"VeroCard FIDO2 Authenticator","973446ca-e21c-9a9b-99f5-9b985a67af0f":"ACS FIDO Authenticator Card","74820b05-a6c9-40f9-8fb0-9f86aca93998":"SafeNet eToken Fusion","1105e4ed-af1d-02ff-ffff-ffffffffffff":"Egomet FIDO2 Authenticator for Android","08987058-cadc-4b81-b6e1-30de50dcbe96":"Windows Hello","a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa":"Security Key NFC by Yubico","0acf3011-bc60-f375-fb53-6f05f43154e0":"Nymi FIDO2 Authenticator","d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":"KEY-ID FIDO2 Authenticator","8eec9bf9-486c-46da-9a67-1fbb4f66b9ed":"HID Crescendo 4000 FIPS","4c50ff10-1057-4fc6-b8ed-43a529530c3c":"ImproveID Authenticator","c611b55c-77b2-4527-8082-590e931b2f08":"GoTrust Idem Key (Consumer profile)","ee041bce-25e5-4cdb-8f86-897fd6418464":"Feitian ePass FIDO2-NFC Authenticator","4b89f401-464e-4745-a520-486ddfc5d80e":"IIST FIDO2 Authenticator","2cd2f727-f6ca-44da-8f48-5c2e5da000a2":"Nitrokey 3 AM","10c70715-2a9a-4de1-b0aa-3cff6d496d39":"eToken Fusion NFC FIPS","efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4":"Safenet eToken FIDO","4b3f8944-d4f2-4d21-bb19-764a986ec160":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","4c0cf95d-2f40-43b5-ba42-4c83a11c04ba":"Feitian BioPass FIDO2 Pro Authenticator","5343502d-5343-5343-6172-644649444f32":"ESS Smart Card Inc. Authenticator","69e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Authenticator","09591fc6-9811-48f7-8f57-b9f23df6413f":"Pone Biometrics OFFPAD Authenticator","912435d9-4a88-42f3-972d-1244b0d51420":"SI0X FIDO CL WRIST v1.0","7e3f3d30-3557-4442-bdae-139312178b39":"RSA DS100","73bb0cd4-e502-49b8-9c6f-b59445bf720b":"YubiKey 5 FIPS Series","39589099-9a75-49fc-afaa-801ca211c62a":"Feitian ePass FIDO-NFC (Enterprise Profile) (CTAP2.1, CTAP2.0, U2F)","149a2021-8ef6-4133-96b8-81f8d5b7f1f5":"Security Key by Yubico with NFC","5df66f62-5b47-43d3-aa1d-a6e31c8dbeb5":"Securitag Assembly Group FIDO Authenticator NFC","09619fbf-d75e-4a62-be1d-fe4d240864ae":"VeriMark(TM) Guard 2.1 Fingerprint Security Key","50cbf15a-238c-4457-8f16-812c43bf3c49":"Ensurity AUTH TouchPro","ee7fa1e0-9539-432f-bd43-9c2fc6d4f311":"VeriMark NFC+ USB-C Security Key","b90e7dc1-316e-4fee-a25a-56a666a670fe":"YubiKey 5 Series with Lightning (Enterprise Profile)","175cd298-83d2-4a26-b637-313c07a6434e":"Chunghwa Telecom FIDO2 Smart Card Authenticator","34744913-4f57-4e6e-a527-e9ec3c4b94e6":"YubiKey Bio Series - Multi-protocol Edition","5ea308b2-7ac7-48b9-ac09-7e2da9015f8c":"Veridium Android SDK","3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":"GoTrust Idem Key FIDO2 Authenticator","46544d5d-8f5d-4db4-89ac-ea8977073fff":"Foongtone FIDO Authenticator","998f358b-2dd2-4cbe-a43a-e8107438dfb3":"OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator","30b5035e-d297-4ff2-010b-addc96ba6a98":"OneSpan DIGIPASS FX2-A","817cdab8-0d51-4de1-a821-e25b88519cf3":"Swissbit iShield Key 2 FIPS","61250591-b2bc-4456-b719-0b17be90bb30":"eWBM eFPA FIDO2 Authenticator","8c39ee86-7f9a-4a95-9ba3-f6b097e5c2ee":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","f8a011f3-8c0a-4d15-8006-17111f9edc7d":"Security Key by Yubico","8976631b-d4a0-427f-5773-0ec71c9e0279":"Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator","516d3969-5a57-5651-5958-4e7a49434167":"SmartDisplayer BobeePass FIDO2 Authenticator","8681a073-5f50-4d52-bce4-e21658d207b3":"RSA Authenticator 4 for iOS","30b5035e-d297-4ff7-030b-addc96ba6a98":"OneSpan DIGIPASS FX7-C","e41b42a3-60ac-4afb-8757-a98f2d7f6c9f":"Deepnet SafeKey/Classic (FP)","c89e6a38-6c00-5426-5aa5-c9cbf48f0382":"ACS FIDO Authenticator NFC","a02167b9-ae71-4ac7-9a07-06432ebb6f1c":"YubiKey 5 Series with Lightning","82b0a720-127a-4788-b56d-d1d4b2d82eac":"ID-One Key","2c0df832-92de-4be1-8412-88a8f074df4a":"Feitian FIDO Smart Card","59f85fe7-faa5-4c92-9f52-697b9d4d5473":"RSA Authenticator 4 for Android","79f3c8ba-9e35-484b-8f47-53a5a0f5c630":"YubiKey 5 FIPS Series with NFC (Enterprise Profile)","def8ab1a-9f91-44f1-a103-088d8dc7d681":"IDEMIA SOLVO Fly 80 R3 FIDO Card e","970c8d9c-19d2-46af-aa32-3f448db49e35":"WinMagic FIDO Eazy - TPM","c5703116-972b-4851-a3e7-ae1259843399":"NEOWAVE Badgeo FIDO2","c80dbd9a-533f-4a17-b941-1a2f1c7cedff":"HID Crescendo C3000","0b8b05a4-ebd4-4b0b-8f5f-33d7b6e606ab":"HID Crescendo 4000","5b0e46ba-db02-44ac-b979-ca9b84f5e335":"YubiKey 5 FIPS Series with Lightning Preview","12755c32-8ad1-46eb-881c-e0b38d848b09":"Feitian ePass FIDO Authenticator (CTAP2.1, CTAP2.0, U2F)","2a55aee6-27cb-42c0-bc6e-04efe999e88a":"HID Crescendo 4000","820d89ed-d65a-409e-85cb-f73f0578f82a":"IDmelon Authenticator","019614a3-2703-7e35-a453-285fd06c5d24":"ATLKey Authenticator","3124e301-f14e-4e38-876d-fbeeb090e7bf":"YubiKey 5 Series with Lightning Preview","b6ede29c-3772-412c-8a78-539c1f4c62d2":"Feitian BioPass FIDO2 Plus Authenticator","ed042a3a-4b22-4455-bb69-a267b652ae7e":"Security Key NFC by Yubico - Enterprise Edition","b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc":"YubiKey 5 Series with NFC - Enhanced PIN (Enterprise Profile)","85203421-48f9-4355-9bc8-8a53846e5083":"YubiKey 5 FIPS Series with Lightning","fcc0118f-cd45-435b-8da1-9782b2da0715":"YubiKey 5 FIPS Series with NFC","d821a7d4-e97c-4cb6-bd82-4237731fd4be":"Hyper FIDO Bio Security Key","9876631b-d4a0-427f-5773-0ec71c9e0279":"Somu Secp256R1 FIDO2 CTAP2 Authenticator","f56f58b3-d711-4afc-ba7d-6ac05f88cb19":"WinMagic FIDO Eazy - Phone","6ec5cff2-a0f9-4169-945b-f33b563f7b99":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","882adaf5-3aa9-4708-8e7d-3957103775b4":"T-Shield TrustSec FIDO2 Bio and client PIN version","49a15c1c-3f63-3f51-23a7-b9e00096edd1":"IDEX CTAP2.1 Biometrics","f4c63eff-d26c-4248-801c-3736c7eaa93a":"FIDO KeyPass S3","d384db22-4d50-ebde-2eac-5765cf1e2a44":"Excelsecu eSecu FIDO2 Fingerprint Security Key","0db01cd6-5618-455b-bb46-1ec203d3213e":"GoldKey Security Token","b93fd961-f2e6-462f-b122-82002247de78":"Android Authenticator","aa79f476-ea00-417e-9628-1e8365123922":"HID Crescendo 4000 FIDO","1e906e14-77af-46bc-ae9f-fe6ef18257e4":"VeridiumID Passkey iOS SDK","2fc0579f-8113-47ea-b116-bb5a8db9202a":"YubiKey 5 Series with NFC","31c3f7ff-bf15-4327-83ec-9336abcbcd34":"WinMagic FIDO Eazy - Software","9ddd1817-af5a-4672-a2b9-3e3dd95000a9":"Windows Hello","d8522d9f-575b-4866-88a9-ba99fa02f35b":"YubiKey Bio Series - FIDO Edition","050dd0bc-ff20-4265-8d5d-305c4b215192":"eToken Fusion FIPS","50a45b0c-80e7-f944-bf29-f552bfa2e048":"ACS FIDO Authenticator","f7c558a0-f465-11e8-b568-0800200c9a66":"KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator","3f59672f-20aa-4afe-b6f4-7e5e916b6d98":"Arculus FIDO 2.1 Key Card [P71]","42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3":"Google Titan Security Key v2","361a3082-0278-4583-a16f-72a527f973e4":"eWBM eFA500 FIDO2 Authenticator","2ffd6452-01da-471f-821b-ea4bf6c8676a":"IDPrime 941 Fido","30b5035e-d297-4ff7-b00b-addc96ba6a98":"OneSpan DIGIPASS FX7","5eaff75a-dd43-451f-af9f-87c9eeae293e":"Swissbit iShield Key 2 FIPS Enterprise","b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4":"ZTPass SmartAuth","692db549-7ae5-44d5-a1e5-dd20a493b723":"HID Crescendo Key","23315ad0-6aca-4ba1-952e-f044f1e36976":"Clife Key 2 NFC","1d1b4e33-76a1-47fb-97a0-14b10d0933f1":"Cryptnox FIDO2.1","bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a":"Excelsecu eSecu FIDO2 PRO Security Key","3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":"Feitian iePass FIDO Authenticator","23786452-f02d-4344-87ed-aaf703726881":"SafeNet eToken Fusion CC","5e264d9d-28ef-4d34-95b4-5941e7a4faa8":"Ideem ZSM FIDO2 Authenticator","d2fbd093-ee62-488d-9dad-1e36389f8826":"YubiKey 5 FIPS Series (RC Preview)","234cd403-35a2-4cc2-8015-77ea280c77f5":"Feitian ePass FIDO2-NFC Series (CTAP2.1, CTAP2.0, U2F)","6999180d-630c-442d-b8f7-424b90a43fae":"Hyper FIDO Pro (CTAP2.1, CTAP2.0, U2F)","662ef48a-95e2-4aaa-a6c1-5b9c40375824":"YubiKey 5 Series with NFC - Enhanced PIN","aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":"HID Crescendo C2300","87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c":"eWBM eFA320 FIDO2 Authenticator","58276709-bb4b-4bb3-baf1-60eea99282a7":"YubiKey Bio Series - Multi-protocol Edition 1VDJSN","7d2afadd-bf6b-44a2-a66b-e831fceb8eff":"Taglio CTAP2.1 EP","30b5035e-d297-4ff1-020b-addc96ba6a98":"OneSpan DIGIPASS FX1-C","20ac7a17-c814-4833-93fe-539f0d5e3389":"YubiKey 5 Series (Enterprise Profile)","9012593f-43e4-4461-a97a-d92777b55d74":"VinCSS FIDO2 Fingerprint","d7781e5d-e353-46aa-afe2-3ca49f13332a":"YubiKey 5 Series with NFC","9f0d8150-baa5-4c00-9299-ad62c8bb4e87":"GoTrust Idem Card FIDO2 Authenticator","12ded745-4bed-47d4-abaa-e713f51d6393":"Feitian AllinOne FIDO2 Authenticator","88bbd2f0-342a-42e7-9729-dd158be5407a":"Precision InnaIT Key FIDO 2 Level 2 certified","1d8cac46-47a1-3386-af50-e88ae46fe802":"Ledger Flex FIDO2 Authenticator","dd86a2da-86a0-4cbe-b462-4bd31f57bc6f":"YubiKey Bio Series - FIDO Edition","773c30d9-5919-4e96-a4f5-db65e95cf890":"GSTAG OAK FIDO2 Authenticator","34f5766d-1536-4a24-9033-0e294e510fb0":"YubiKey 5 Series with NFC Preview","83c47309-aabb-4108-8470-8be838b573cb":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","4e2ddbc2-2687-4709-8551-cb66c9776bfe":"SECORA ID V2 FIDO2.1 L1","be727034-574a-f799-5c76-0929e0430973":"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)","092277e5-8437-46b5-b911-ea64b294acb7":"Taglio CTAP2.1 CS","ca87cb70-4c1b-4579-a8e8-4efdd7c007e0":"FIDO Alliance TruU Sample FIDO2 Authenticator","23195a52-62d9-40fa-8ee5-23b173f4fb52":"Hyper FIDO Pro NFC","a7fc3f84-86a3-4da4-a3d7-eb6485a066d8":"NEOWAVE Badgeo FIDO2 (CTAP 2.1)","9e66c661-e428-452a-a8fb-51f7ed088acf":"YubiKey 5 FIPS Series with Lightning (RC Preview)","58b44d0b-0a7c-f33a-fd48-f7153c871352":"Ledger Nano S Plus FIDO2 Authenticator","454e5346-4944-4ffd-6c93-8e9267193e9b":"Ensurity AUTH BioPro","146e77ef-11eb-4423-b847-ce77864e9411":"eToken Fusion NFC PIV","13ac47cf-1d78-4fd5-9060-aedaabacf826":"HID Crescendo Key V3 - Enterprise Edition","e77e3c64-05e3-428b-8824-0cbeb04b829d":"Security Key NFC by Yubico","33d6d7d0-279f-4ef3-96b3-2d3282f4bde6":"Thales eToken Fusion BIO Enterprise","8d4378b0-725d-4432-b3c2-01fcdaf46286":"VeridiumID Passkey Android SDK","7409272d-1ff9-4e10-9fc9-ac0019c124fd":"YubiKey Bio Series - FIDO Edition","bb66c294-de08-47e4-b7aa-d12c2cd3fb20":"Mettlesemi Vishwaas Hawk Authenticator using FIDO2","c4ddaf11-3032-4e77-b3b9-3a340369b9ad":"HID Crescendo Fusion","7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3":"YubiKey Bio Series - Multi-protocol Edition","07a9f89c-6407-4594-9d56-621d5f1e358b":"NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator","d61d3b87-3e7c-4aea-9c50-441c371903ad":"KeyVault Secp256R1 FIDO2 CTAP2 Authenticator","c62100de-759b-4bf8-b22b-63b3e3a80401":"Token Ring 3 FIDO2 Authenticator","5ca1ab1e-1337-fa57-f1d0-a117e71ca702":"Allthenticator iOS App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","b92c3f9a-c014-4056-887f-140a2501163b":"Security Key by Yubico","54d9fee8-e621-4291-8b18-7157b99c5bec":"HID Crescendo Enabled","a25342c0-3cdc-4414-8e46-f4807fca511c":"YubiKey 5 Series with NFC","3a662962-c6d4-4023-bebb-98ae92e78e20":"YubiKey 5 FIPS Series with Lightning (Enterprise Profile)","20f0be98-9af9-986a-4b42-8eca4acb28e4":"Excelsecu eSecu FIDO2 Fingerprint Security Key","ca4cff1b-5a81-4404-8194-59aabcf1660b":"IDPrime 3930 FIDO","ab32f0c6-2239-afbb-c470-d2ef4e254db6":"TEST (DUMMY RECORD)","760eda36-00aa-4d29-855b-4012a182cdeb":"Security Key NFC by Yubico Preview","6028b017-b1d4-4c02-b4b3-afcdafc96bb2":"Windows Hello","b12eac35-586c-4809-a4b1-d81af6c305cf":"Deepnet SafeKey/Classic (NFC)","30b5035e-d297-4fc1-b00b-addc96ba6a97":"OneSpan FIDO Touch","560a780c-b6ae-4f03-b110-082f856425b4":"KQC QuKey Bio FIDO2 Authenticator","1ac71f64-468d-4fe0-bef1-0e5f2f551f18":"YubiKey 5 Series with NFC (Enterprise Profile)","6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":"Security Key by Yubico with NFC","9eb85bb6-9625-4a72-815d-0487830ccab2":"Ensurity AUTH BioPro Desktop","30b5035e-d297-4ff7-010b-addc96ba6a98":"OneSpan DIGIPASS FX7-B","5ca1ab1e-fa57-1337-f1d0-a117371ca702":"Allthenticator Android App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","eabb46cc-e241-80bf-ae9e-96fa6d2975cf":"TOKEN2 PIN Plus Security Key Series ","53414d53-554e-4700-0000-000000000000":"Samsung Pass","e416201b-afeb-41ca-a03d-2281c28322aa":"ATKey.Pro CTAP2.1","905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7":"YubiKey 5 FIPS Series (Enterprise Profile)","cfcb13a2-244f-4b36-9077-82b79d6a7de7":"USB/NFC Passcode Authenticator","76692dc1-c56a-48d9-8e7d-31b5ced430ac":"VeriMark NFC+ USB-A Security Key","91ad6b93-264b-4987-8737-3a690cad6917":"Token Ring FIDO2 Authenticator","a02140b7-0cbd-42e1-a9b5-a39da2545114":"Feitian BioPass FIDO2 Plus (Enterprise Profile)","5753362b-4e6b-6345-7b2f-255438404c75":"WiSECURE Blentity FIDO2 Authenticator","9f77e279-a6e2-4d58-b700-31e5943c6a98":"Hyper FIDO Pro","b9f6b7b6-f929-4189-bca9-dd951240c132":"Deepnet SafeKey/Classic (USB)","cc45f64e-52a2-451b-831a-4edd8022a202":"ToothPic Passkey Provider","0bb43545-fd2c-4185-87dd-feb0b2916ace":"Security Key NFC by Yubico - Enterprise Edition","73402251-f2a8-4f03-873e-3cb6db604b03":"uTrust FIDO2 Security Key","c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":"YubiKey 5 FIPS Series with NFC","70e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Bluetooth Authenticator","4fc84f16-2545-4e53-b8fc-7bf4d7282a10":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","6ab56fad-881f-4a43-acb2-0be065924522":"YubiKey 5 Series with NFC (Enterprise Profile)","504d7149-4e4c-3841-4555-55445a677357":"WiSECURE AuthTron USB FIDO2 Authenticator","2c2aeed8-8174-4159-814b-486e92a261d0":"NEOWAVE WINKEO V2.0","f2145e86-211e-4931-b874-e22bba7d01cc":"ID-One Key","a3975549-b191-fd67-b8fb-017e2917fdb3":"Excelsecu eSecu FIDO2 NFC Security Key","19083c3d-8383-4b18-bc03-8f1c9ab2fd1b":"YubiKey 5 Series","da1fa263-8b25-42b6-a820-c0036f21ba7f":"ATKey.Card NFC","6002f033-3c07-ce3e-d0f7-0ffe5ed42543":"Excelsecu eSecu FIDO2 Fingerprint Key","5fdb81b8-53f0-4967-a881-f5ec26fe4d18":"VinCSS FIDO2 Authenticator","78ba3993-d784-4f44-8d6e-cc0a8ad5230e":"Feitian ePass FIDO-NFC(CTAP2.1, CTAP2.0, U2F)","57f7de54-c807-4eab-b1c6-1c9be7984e92":"YubiKey 5 FIPS Series","bb405265-40cf-4115-93e5-a332c1968d8c":"ID-One Card","2d3bec26-15ee-4f5d-88b2-53622490270b":"HID Crescendo Key V2","489ff376-b48d-6640-bb69-782a860ca795":"Mettlesemi Vishwaas Eagle Authenticator using FIDO2","3b24bf49-1d45-4484-a917-13175df0867b":"YubiKey 5 Series with Lightning (Enterprise Profile)","30b5035e-d297-4ff1-010b-addc96ba6a98":"OneSpan DIGIPASS FX1a","cb69481e-8ff7-4039-93ec-0a2729a154a8":"YubiKey 5 Series","0076631b-d4a0-427f-5773-0ec71c9e0279":"HYPR FIDO2 Authenticator","d716019a-9f4e-4041-9750-17c78f8ae81a":"eToken Fusion BIO","57235694-51a5-4a4d-a81a-f42185df6502":"SHALO AUTH","24673149-6c86-42e7-98d9-433fb5b73296":"YubiKey 5 Series with Lightning","42df17de-06ba-4177-a2bb-6701be1380d6":"Feitian BioPass FIDO2 Plus Authenticator","d7a423ad-3e19-4492-9200-78137dccc136":"VivoKey Apex FIDO2","ba76a271-6eb6-4171-874d-b6428dbe3437":"ATKey.ProS","97e6a830-c952-4740-95fc-7c78dc97ce47":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","f573f209-b7fb-b261-671a-d7cf624cc812":"Excelsecu eSecu FIDO2 PRO+ Security Key","6e24d385-004a-16a0-7bfe-efd963845b34":"Ledger Stax FIDO2 Authenticator","ee882879-721c-4913-9775-3dfcce97072a":"YubiKey 5 Series","8876631b-d4a0-427f-5773-0ec71c9e0279":"Solo Secp256R1 FIDO2 CTAP2 Authenticator","fec067a1-f1d0-4c5e-b4c0-cc3237475461":"KX701 SmartToken FIDO","30b5035e-d297-4ff1-b00b-addc96ba6a98":"OneSpan DIGIPASS FX1 BIO","b267239b-954f-4041-a01b-ee4f33c145b6":"authenton1 - CTAP2.1","b50d5e0a-7f81-4959-9b12-f45407407503":"IDPrime 3940 FIDO","8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":"FT-JCOS FIDO Fingerprint Card","99bf4610-ec26-4252-b31f-7380ccd59db5":"ZTPass SmartAuth","a1f52be5-dfab-4364-b51c-2bd496b14a56":"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR","0f00cc22-4640-41e7-9585-384ec73ffe9b":"Taglio CTAP2.1 BIO","ff4dac45-ede8-4ec2-aced-cf66103f4335":"YubiKey 5 Series","ba86dc56-635f-4141-aef6-00227b1b9af6":"TruU Windows Authenticator","3e078ffd-4c54-4586-8baa-a77da113aec5":"Hideez Key 3 FIDO2","fc5ca237-69a0-4f3c-afe4-1ebc66def6df":"Clife Key 2","ec31b4cc-2acc-4b8e-9c01-bade00ccbe26":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","5d629218-d3a5-11ed-afa1-0242ac120002":"Swissbit iShield Key Pro","bb878d7b-cf54-4784-b390-357030497043":"TruU FIDO2 Authenticator","d41f5a69-b817-4144-a13c-9ebd6d9254d6":"ATKey.Card CTAP2.0","e86addcd-7711-47e5-b42a-c18257b0bf61":"IDCore 3121 Fido","b113a455-cfb6-4c17-8cba-cd952feb7d48":"eToken FIDO NFC","95442b2e-f15e-4def-b270-efb106facb4e":"eWBM eFA310 FIDO2 Authenticator","dda9aa35-aaf1-4d3c-b6db-7902fd7dbbbf":"IDEMIA SOLVO Fly 80 R3 FIDO Card c","cdbdaea2-c415-5073-50f7-c04e968640b6":"Excelsecu eSecu FIDO2 Security Key","3aa78eb1-ddd8-46a8-a821-8f8ec57a7bd5":"YubiKey 5 CCN Series with NFC","bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":"OCTATCO EzQuant FIDO2 AUTHENTICATOR","eb3b131e-59dc-536a-d176-cb7306da10f5":"ellipticSecure MIRkey USB Authenticator","3fd410dc-8ab7-4b86-a1cb-c7174620b2dc":"IDEMIA SOLVO Fly 80 R1 FIDO Card Draft","a6c5f5d8-2ad0-48b6-8257-e502c8970931":"eToken FIDO NFC Enterprise","e400ef8c-711d-4692-af46-7f2cf7da23ad":"Swissbit iShield Key 2 Enterprise","87c13177-85d6-40ac-8c61-fe7ab3de9dfb":"HID Crescendo Key V3","1c086528-58d5-f211-823c-356786e36140":"Atos CardOS FIDO2","77010bd7-212a-4fc9-b236-d2ca5e9d4084":"Feitian BioPass FIDO2 Authenticator","d94a29d9-52dd-4247-9c2d-8b818b610389":"VeriMark Guard Fingerprint Key","7b96457d-e3cd-432b-9ceb-c9fdd7ef7432":"YubiKey 5 FIPS Series with Lightning","7991798a-a7f3-487f-98c0-3faf7a458a04":"HID Crescendo Key V3","833b721a-ff5f-4d00-bb2e-bdda3ec01e29":"Feitian ePass FIDO2 Authenticator","c89674e3-a765-4b07-888a-7c086fbdf04b":"StarSign FIDO Card","a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942":"AliasVault","ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4":"Google Password Manager","adce0002-35bc-c60a-648b-0b25f1f05503":"Chrome on Mac","dd4ec289-e01d-41c9-bb89-70fa845d4bf2":"iCloud Keychain (Managed)","531126d6-e717-415c-9320-3d9aa6981239":"Dashlane","bada5566-a7aa-401f-bd96-45619a55120d":"1Password","b84e4048-15dc-4dd0-8640-f4f60813c8af":"NordPass","0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6":"Keeper","891494da-2c90-4d31-a9cd-4eab0aed1309":"Sésame","f3809540-7f14-49c1-a8b3-8f813b225541":"Enpass","b5397666-4885-aa6b-cebf-e52262a439a2":"Chromium Browser","771b48fd-d3d4-4f74-9232-fc157ab0507a":"Edge on Mac","d548826e-79b4-db40-a3d8-11116f7e8349":"Bitwarden","fbfc3007-154e-4ecc-8c0b-6e020557d7bd":"Apple Passwords","66a0ccb3-bd6a-191f-ee06-e375c50b9846":"Thales Bio iOS SDK","8836336a-f590-0921-301d-46427531eee6":"Thales Bio Android SDK","cd69adb5-3c7a-deb9-3177-6800ea6cb72a":"Thales PIN Android SDK","17290f1e-c212-34d0-1423-365d729f09d9":"Thales PIN iOS SDK","50726f74-6f6e-5061-7373-50726f746f6e":"Proton Pass","fdb141b2-5d84-443e-8a35-4698c205a502":"KeePassXC","eaecdef2-1c31-5634-8639-f1cbd9c00a08":"KeePassDX","bfc748bb-3429-4faa-b9f9-7cfa9f3b76d0":"iPasswords","b35a26b2-8f6e-4697-ab1d-d44db4da28c6":"Zoho Vault","b78a0a55-6ef8-d246-a042-ba0f6d55050c":"LastPass","de503f9c-21a4-4f76-b4b7-558eb55c6f89":"Devolutions","22248c4c-7a12-46e2-9a41-44291b373a4d":"LogMeOnce","a10c6dd9-465e-4226-8198-c7c44b91c555":"Kaspersky Password Manager","d350af52-0351-4ba2-acd3-dfeeadc3f764":"pwSafe","d3452668-01fd-4c12-926c-83a4204853aa":"Microsoft Password Manager","6d212b28-a2c1-4638-b375-5932070f62e9":"initial","d49b2120-b865-4191-8cea-be84a52b0485":"Heimlane Vault","e8b7f4a2-c3d5-e6f7-890a-b1c2d3e4f567":"Sherlocked"} diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 9ad56c28..e8d6b617 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -5,4 +5,5 @@ yarn.lock # Compiled files .svelte-kit/ -build/ \ No newline at end of file +build/ +src/lib/paraglide/messages \ No newline at end of file diff --git a/frontend/messages/cs.json b/frontend/messages/cs.json index 0368fb55..8bf78ad7 100644 --- a/frontend/messages/cs.json +++ b/frontend/messages/cs.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Zadejte kód, který byl zobrazen v předchozím kroku.", "authorize": "Autorizovat", "federated_client_credentials": "Údaje o klientovi ve federaci", - "federated_client_credentials_description": "Pomocí federovaných přihlašovacích údajů klienta můžete ověřit klienty OIDC pomocí JWT tokenů vydaných třetí stranou.", "add_federated_client_credential": "Přidat údaje federovaného klienta", "add_another_federated_client_credential": "Přidat dalšího federovaného klienta", "oidc_allowed_group_count": "Počet povolených skupin", diff --git a/frontend/messages/da.json b/frontend/messages/da.json index 9afdca79..5d2b3336 100644 --- a/frontend/messages/da.json +++ b/frontend/messages/da.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Indtast koden, der blev vist i det forrige trin.", "authorize": "Godkend", "federated_client_credentials": "Federated klientlegitimationsoplysninger", - "federated_client_credentials_description": "Ved hjælp af federated klientlegitimationsoplysninger kan du godkende OIDC-klienter med JWT-tokens udstedt af tredjepartsudbydere.", "add_federated_client_credential": "Tilføj federated klientlegitimation", "add_another_federated_client_credential": "Tilføj endnu en federated klientlegitimation", "oidc_allowed_group_count": "Tilladt antal grupper", diff --git a/frontend/messages/de.json b/frontend/messages/de.json index bffc38c6..79885fcc 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Gib den Code ein, der im vorherigen Schritt angezeigt wurde.", "authorize": "Autorisieren", "federated_client_credentials": "Federated Client Credentials", - "federated_client_credentials_description": "Mit Hilfe von Verbund-Client-Anmeldeinformationen kannst du OIDC-Clients mit JWT-Tokens authentifizieren, die von Drittanbietern ausgestellt wurden.", "add_federated_client_credential": "Föderierte Client-Anmeldeinfos hinzufügen", "add_another_federated_client_credential": "Weitere Anmeldeinformationen für einen Verbundclient hinzufügen", "oidc_allowed_group_count": "Erlaubte Gruppenanzahl", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index e328330e..e89938b8 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -365,7 +365,7 @@ "enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.", "authorize": "Authorize", "federated_client_credentials": "Federated Client Credentials", - "federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.", + "federated_client_credentials_description": "Federated client credentials allow authenticating OIDC clients without managing long-lived secrets. They leverage JWT tokens issued by third-party authorities for client assertions, e.g. workload identity tokens.", "add_federated_client_credential": "Add Federated Client Credential", "add_another_federated_client_credential": "Add another federated client credential", "oidc_allowed_group_count": "Allowed Group Count", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 9c1cce93..ac6fdd78 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Introduce el código que se mostró en el paso anterior.", "authorize": "Autorizar", "federated_client_credentials": "Credenciales de cliente federadas", - "federated_client_credentials_description": "Mediante credenciales de cliente federadas, puedes autenticar clientes OIDC utilizando tokens JWT emitidos por autoridades de terceros.", "add_federated_client_credential": "Añadir credenciales de cliente federado", "add_another_federated_client_credential": "Añadir otra credencial de cliente federado", "oidc_allowed_group_count": "Recuento de grupos permitidos", diff --git a/frontend/messages/et.json b/frontend/messages/et.json index e328330e..d3aedc7c 100644 --- a/frontend/messages/et.json +++ b/frontend/messages/et.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.", "authorize": "Authorize", "federated_client_credentials": "Federated Client Credentials", - "federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.", "add_federated_client_credential": "Add Federated Client Credential", "add_another_federated_client_credential": "Add another federated client credential", "oidc_allowed_group_count": "Allowed Group Count", diff --git a/frontend/messages/fi.json b/frontend/messages/fi.json index b8ba5899..59d1f041 100644 --- a/frontend/messages/fi.json +++ b/frontend/messages/fi.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Syötä edellisessä vaiheessa näkynyt koodi.", "authorize": "Salli", "federated_client_credentials": "Federoidut asiakastunnukset", - "federated_client_credentials_description": "Yhdistettyjen asiakastunnistetietojen avulla voit todentaa OIDC-asiakkaat kolmannen osapuolen myöntämillä JWT-tunnuksilla.", "add_federated_client_credential": "Lisää federoitu asiakastunnus", "add_another_federated_client_credential": "Lisää toinen federoitu asiakastunnus", "oidc_allowed_group_count": "Sallittujen ryhmien määrä", diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index aa9f71b2..9ff785d4 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Entrez le code affiché à l'étape précédente.", "authorize": "Autoriser", "federated_client_credentials": "Identifiants client fédérés", - "federated_client_credentials_description": "Avec des identifiants clients fédérés, vous pouvez authentifier des clients OIDC avec des tokens JWT émis par des autorités tierces.", "add_federated_client_credential": "Ajouter un identifiant client fédéré", "add_another_federated_client_credential": "Ajouter un autre identifiant client fédéré", "oidc_allowed_group_count": "Nombre de groupes autorisés", diff --git a/frontend/messages/it.json b/frontend/messages/it.json index 890ad08c..e4337b43 100644 --- a/frontend/messages/it.json +++ b/frontend/messages/it.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Inserisci il codice visualizzato nel passaggio precedente.", "authorize": "Autorizza", "federated_client_credentials": "Identità Federate", - "federated_client_credentials_description": "Utilizzando identità federate, è possibile autenticare i client OIDC utilizzando i token JWT emessi da autorità di terze parti.", "add_federated_client_credential": "Aggiungi Identità Federata", "add_another_federated_client_credential": "Aggiungi un'altra identità federata", "oidc_allowed_group_count": "Numero Gruppi Consentiti", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index fa8c024c..2f480471 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。", "authorize": "Authorize", "federated_client_credentials": "連携クライアントの資格情報", - "federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.", "add_federated_client_credential": "Add Federated Client Credential", "add_another_federated_client_credential": "Add another federated client credential", "oidc_allowed_group_count": "許可されたグループ数", diff --git a/frontend/messages/ko.json b/frontend/messages/ko.json index 0de41375..6ec07029 100644 --- a/frontend/messages/ko.json +++ b/frontend/messages/ko.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "이전 단계에 표시된 코드를 입력하세요.", "authorize": "승인", "federated_client_credentials": "연동 클라이언트 자격 증명", - "federated_client_credentials_description": "연동 클라이언트 자격 증명을 이용하여, OIDC 클라이언트를 제3자 인증 기관에서 발급한 JWT 토큰을 이용해 인증할 수 있습니다.", "add_federated_client_credential": "연동 클라이언트 자격 증명 추가", "add_another_federated_client_credential": "다른 연동 클라이언트 자격 증명 추가", "oidc_allowed_group_count": "허용된 그룹 수", diff --git a/frontend/messages/lv.json b/frontend/messages/lv.json new file mode 100644 index 00000000..e89938b8 --- /dev/null +++ b/frontend/messages/lv.json @@ -0,0 +1,525 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "my_account": "My Account", + "logout": "Logout", + "confirm": "Confirm", + "docs": "Docs", + "key": "Key", + "value": "Value", + "remove_custom_claim": "Remove custom claim", + "add_custom_claim": "Add custom claim", + "add_another": "Add another", + "select_a_date": "Select a date", + "select_file": "Select File", + "profile_picture": "Profile Picture", + "profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.", + "click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.", + "image_should_be_in_format": "The image should be in PNG, JPEG or WEBP format.", + "items_per_page": "Items per page", + "no_items_found": "No items found", + "select_items": "Select items...", + "search": "Search...", + "expand_card": "Expand card", + "copied": "Copied", + "click_to_copy": "Click to copy", + "something_went_wrong": "Something went wrong", + "go_back_to_home": "Go back to home", + "alternative_sign_in_methods": "Alternative Sign In Methods", + "login_background": "Login background", + "logo": "Logo", + "login_code": "Login Code", + "create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.", + "one_hour": "1 hour", + "twelve_hours": "12 hours", + "one_day": "1 day", + "one_week": "1 week", + "one_month": "1 month", + "expiration": "Expiration", + "generate_code": "Generate Code", + "name": "Name", + "browser_unsupported": "Browser unsupported", + "this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please use an alternative sign in method.", + "an_unknown_error_occurred": "An unknown error occurred", + "authentication_process_was_aborted": "The authentication process was aborted", + "error_occurred_with_authenticator": "An error occurred with the authenticator", + "authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials", + "authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys", + "passkey_was_previously_registered": "This passkey was previously registered", + "authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms", + "webauthn_error_invalid_rp_id": "The configured relying party ID is invalid.", + "webauthn_error_invalid_domain": "The configured domain is invalid.", + "contact_administrator_to_fix": "Contact your administrator to fix this issue.", + "webauthn_operation_not_allowed_or_timed_out": "The operation was not allowed or timed out", + "webauthn_not_supported_by_browser": "Passkeys are not supported by this browser. Please use an alternative sign in method.", + "critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.", + "sign_in_to": "Sign in to {name}", + "client_not_found": "Client not found", + "client_wants_to_access_the_following_information": "{client} wants to access the following information:", + "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to {client} with your {appName} account?", + "email": "Email", + "view_your_email_address": "View your email address", + "profile": "Profile", + "view_your_profile_information": "View your profile information", + "groups": "Groups", + "view_the_groups_you_are_a_member_of": "View the groups you are a member of", + "cancel": "Cancel", + "sign_in": "Sign in", + "try_again": "Try again", + "client_logo": "Client Logo", + "sign_out": "Sign out", + "do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of {appName} with the account {username}?", + "sign_in_to_appname": "Sign in to {appName}", + "please_try_to_sign_in_again": "Please try to sign in again.", + "authenticate_with_passkey_to_access_account": "Authenticate yourself with your passkey to access your account.", + "authenticate": "Authenticate", + "please_try_again": "Please try again.", + "continue": "Continue", + "alternative_sign_in": "Alternative Sign In", + "if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you don't have access to your passkey, you can sign in using one of the following methods.", + "use_your_passkey_instead": "Use your passkey instead?", + "email_login": "Email Login", + "enter_a_login_code_to_sign_in": "Enter a login code to sign in.", + "sign_in_with_login_code": "Sign in with login code", + "request_a_login_code_via_email": "Request a login code via email.", + "go_back": "Go back", + "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.", + "enter_code": "Enter code", + "enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.", + "your_email": "Your email", + "submit": "Submit", + "enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.", + "code": "Code", + "invalid_redirect_url": "Invalid redirect URL", + "audit_log": "Audit Log", + "users": "Users", + "user_groups": "User Groups", + "oidc_clients": "OIDC Clients", + "api_keys": "API Keys", + "application_configuration": "Application Configuration", + "settings": "Settings", + "update_pocket_id": "Update Pocket ID", + "powered_by": "Powered by", + "see_your_recent_account_activities": "See your account activities within the configured retention period.", + "time": "Time", + "event": "Event", + "approximate_location": "Approximate Location", + "ip_address": "IP Address", + "device": "Device", + "client": "Client", + "unknown": "Unknown", + "account_details_updated_successfully": "Account details updated successfully", + "profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.", + "account_settings": "Account Settings", + "passkey_missing": "Passkey missing", + "please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Please add a passkey to prevent losing access to your account.", + "single_passkey_configured": "Single Passkey Configured", + "it_is_recommended_to_add_more_than_one_passkey": "It is recommended to add more than one passkey to avoid losing access to your account.", + "account_details": "Account Details", + "passkeys": "Passkeys", + "manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.", + "add_passkey": "Add Passkey", + "create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.", + "create": "Create", + "first_name": "First name", + "last_name": "Last name", + "username": "Username", + "save": "Save", + "username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols", + "username_must_start_with": "Username must start with an alphanumeric character", + "username_must_end_with": "Username must end with an alphanumeric character", + "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.", + "or_visit": "or visit", + "added_on": "Added on", + "rename": "Rename", + "delete": "Delete", + "are_you_sure_you_want_to_delete_this_passkey": "Are you sure you want to delete this passkey?", + "passkey_deleted_successfully": "Passkey deleted successfully", + "delete_passkey_name": "Delete {passkeyName}", + "passkey_name_updated_successfully": "Passkey name updated successfully", + "name_passkey": "Name Passkey", + "name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.", + "create_api_key": "Create API Key", + "add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access to the Pocket ID API.", + "add_api_key": "Add API Key", + "manage_api_keys": "Manage API Keys", + "api_key_created": "API Key Created", + "for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.", + "description": "Description", + "api_key": "API Key", + "close": "Close", + "name_to_identify_this_api_key": "Name to identify this API key.", + "expires_at": "Expires At", + "when_this_api_key_will_expire": "When this API key will expire.", + "optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.", + "expiration_date_must_be_in_the_future": "Expiration date must be in the future", + "revoke_api_key": "Revoke API Key", + "never": "Never", + "revoke": "Revoke", + "api_key_revoked_successfully": "API key revoked successfully", + "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.", + "last_used": "Last Used", + "actions": "Actions", + "images_updated_successfully": "Images updated successfully. It may take a few minutes to update.", + "general": "General", + "configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.", + "ldap": "LDAP", + "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.", + "images": "Images", + "update": "Update", + "email_configuration_updated_successfully": "Email configuration updated successfully", + "save_changes_question": "Save changes?", + "you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?", + "save_and_send": "Save and send", + "test_email_sent_successfully": "Test email sent successfully to your email address.", + "failed_to_send_test_email": "Failed to send test email. Check the server logs for more information.", + "smtp_configuration": "SMTP Configuration", + "smtp_host": "SMTP Host", + "smtp_port": "SMTP Port", + "smtp_user": "SMTP User", + "smtp_password": "SMTP Password", + "smtp_from": "SMTP From", + "smtp_tls_option": "SMTP TLS Option", + "email_tls_option": "Email TLS Option", + "skip_certificate_verification": "Skip Certificate Verification", + "this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.", + "enabled_emails": "Enabled Emails", + "email_login_notification": "Email Login Notification", + "send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.", + "emai_login_code_requested_by_user": "Email Login Code Requested by User", + "allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.", + "email_login_code_from_admin": "Email Login Code from Admin", + "allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.", + "send_test_email": "Send test email", + "application_configuration_updated_successfully": "Application configuration updated successfully", + "application_name": "Application Name", + "session_duration": "Session Duration", + "the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "The duration of a session in minutes before the user has to sign in again.", + "enable_self_account_editing": "Enable Self-Account Editing", + "whether_the_users_should_be_able_to_edit_their_own_account_details": "Whether the users should be able to edit their own account details.", + "ldap_configuration_updated_successfully": "LDAP configuration updated successfully", + "ldap_disabled_successfully": "LDAP disabled successfully", + "ldap_sync_finished": "LDAP sync finished", + "client_configuration": "Client Configuration", + "ldap_url": "LDAP URL", + "ldap_bind_dn": "LDAP Bind DN", + "ldap_bind_password": "LDAP Bind Password", + "ldap_base_dn": "LDAP Base DN", + "user_search_filter": "User Search Filter", + "the_search_filter_to_use_to_search_or_sync_users": "The Search filter to use to search/sync users.", + "groups_search_filter": "Groups Search Filter", + "the_search_filter_to_use_to_search_or_sync_groups": "The Search filter to use to search/sync groups.", + "attribute_mapping": "Attribute Mapping", + "user_unique_identifier_attribute": "User Unique Identifier Attribute", + "the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.", + "username_attribute": "Username Attribute", + "user_mail_attribute": "User Mail Attribute", + "user_first_name_attribute": "User First Name Attribute", + "user_last_name_attribute": "User Last Name Attribute", + "user_profile_picture_attribute": "User Profile Picture Attribute", + "the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "The value of this attribute can either be a URL, a binary or a base64 encoded image.", + "group_members_attribute": "Group Members Attribute", + "the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.", + "group_unique_identifier_attribute": "Group Unique Identifier Attribute", + "group_rdn_attribute": "Group RDN Attribute (in DN)", + "admin_group_name": "Admin Group Name", + "members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.", + "disable": "Disable", + "sync_now": "Sync now", + "enable": "Enable", + "user_created_successfully": "User created successfully", + "create_user": "Create User", + "add_a_new_user_to_appname": "Add a new user to {appName}", + "add_user": "Add User", + "manage_users": "Manage Users", + "admin_privileges": "Admin Privileges", + "admins_have_full_access_to_the_admin_panel": "Admins have full access to the admin panel.", + "delete_firstname_lastname": "Delete {firstName} {lastName}", + "are_you_sure_you_want_to_delete_this_user": "Are you sure you want to delete this user?", + "user_deleted_successfully": "User deleted successfully", + "role": "Role", + "source": "Source", + "admin": "Admin", + "user": "User", + "local": "Local", + "toggle_menu": "Toggle menu", + "edit": "Edit", + "user_groups_updated_successfully": "User groups updated successfully", + "user_updated_successfully": "User updated successfully", + "custom_claims_updated_successfully": "Custom claims updated successfully", + "back": "Back", + "user_details_firstname_lastname": "User Details {firstName} {lastName}", + "manage_which_groups_this_user_belongs_to": "Manage which groups this user belongs to.", + "custom_claims": "Custom Claims", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested.", + "user_group_created_successfully": "User group created successfully", + "create_user_group": "Create User Group", + "create_a_new_group_that_can_be_assigned_to_users": "Create a new group that can be assigned to users.", + "add_group": "Add Group", + "manage_user_groups": "Manage User Groups", + "friendly_name": "Friendly Name", + "name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI", + "name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim", + "delete_name": "Delete {name}", + "are_you_sure_you_want_to_delete_this_user_group": "Are you sure you want to delete this user group?", + "user_group_deleted_successfully": "User group deleted successfully", + "user_count": "User Count", + "user_group_updated_successfully": "User group updated successfully", + "users_updated_successfully": "Users updated successfully", + "user_group_details_name": "User Group Details {name}", + "assign_users_to_this_group": "Assign users to this group.", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts.", + "oidc_client_created_successfully": "OIDC client created successfully", + "create_oidc_client": "Create OIDC Client", + "add_a_new_oidc_client_to_appname": "Add a new OIDC client to {appName}.", + "add_oidc_client": "Add OIDC Client", + "manage_oidc_clients": "Manage OIDC Clients", + "one_time_link": "One Time Link", + "use_this_link_to_sign_in_once": "Use this link to sign in once. This is needed for users who haven't added a passkey yet or have lost it.", + "add": "Add", + "callback_urls": "Callback URLs", + "logout_callback_urls": "Logout Callback URLs", + "public_client": "Public Client", + "public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.", + "pkce": "PKCE", + "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.", + "requires_reauthentication": "Requires Re-Authentication", + "requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in", + "name_logo": "{name} logo", + "change_logo": "Change Logo", + "upload_logo": "Upload Logo", + "remove_logo": "Remove Logo", + "are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?", + "oidc_client_deleted_successfully": "OIDC client deleted successfully", + "authorization_url": "Authorization URL", + "oidc_discovery_url": "OIDC Discovery URL", + "token_url": "Token URL", + "userinfo_url": "Userinfo URL", + "logout_url": "Logout URL", + "certificate_url": "Certificate URL", + "enabled": "Enabled", + "disabled": "Disabled", + "oidc_client_updated_successfully": "OIDC client updated successfully", + "create_new_client_secret": "Create new client secret", + "are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.", + "generate": "Generate", + "new_client_secret_created_successfully": "New client secret created successfully", + "oidc_client_name": "OIDC Client {name}", + "client_id": "Client ID", + "client_secret": "Client secret", + "show_more_details": "Show more details", + "allowed_user_groups": "Allowed User Groups", + "allowed_user_groups_description": "Select the user groups whose members are allowed to sign in to this client.", + "allowed_user_groups_status_unrestricted_description": "No user group restrictions are applied. Any user can sign in to this client.", + "unrestrict": "Unrestrict", + "restrict": "Restrict", + "user_groups_restriction_updated_successfully": "User groups restriction updated successfully", + "allowed_user_groups_updated_successfully": "Allowed user groups updated successfully", + "favicon": "Favicon", + "light_mode_logo": "Light Mode Logo", + "dark_mode_logo": "Dark Mode Logo", + "email_logo": "Email Logo", + "background_image": "Background Image", + "language": "Language", + "reset_profile_picture_question": "Reset profile picture?", + "this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "This will remove the uploaded image and reset the profile picture to default. Do you want to continue?", + "reset": "Reset", + "reset_to_default": "Reset to default", + "profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.", + "select_the_language_you_want_to_use": "Select the language you want to use. Please note that some text may be automatically translated and could be inaccurate.", + "contribute_to_translation": "If you find an issue you're welcome to contribute to the translation on Crowdin.", + "personal": "Personal", + "global": "Global", + "all_users": "All Users", + "all_events": "All Events", + "all_clients": "All Clients", + "all_locations": "All Locations", + "global_audit_log": "Global Audit Log", + "see_all_recent_account_activities": "View the account activities of all users during the set retention period.", + "token_sign_in": "Token Sign In", + "client_authorization": "Client Authorization", + "new_client_authorization": "New Client Authorization", + "device_code_authorization": "Device Code Authorization", + "new_device_code_authorization": "New Device Code Authorization", + "passkey_added": "Passkey Added", + "passkey_removed": "Passkey Removed", + "disable_animations": "Disable Animations", + "turn_off_ui_animations": "Turn off animations throughout the UI.", + "user_disabled": "Account Disabled", + "disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.", + "user_disabled_successfully": "User has been disabled successfully.", + "user_enabled_successfully": "User has been enabled successfully.", + "status": "Status", + "disable_firstname_lastname": "Disable {firstName} {lastName}", + "are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.", + "ldap_soft_delete_users": "Keep disabled users from LDAP.", + "ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.", + "login_code_email_success": "The login code has been sent to the user.", + "send_email": "Send Email", + "show_code": "Show Code", + "callback_url_description": "URL(s) provided by your client. Will be automatically added if left blank. Wildcards are supported.", + "logout_callback_url_description": "URL(s) provided by your client for logout. Wildcards are supported.", + "api_key_expiration": "API Key Expiration", + "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.", + "authorize_device": "Authorize Device", + "the_device_has_been_authorized": "The device has been authorized.", + "enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.", + "authorize": "Authorize", + "federated_client_credentials": "Federated Client Credentials", + "federated_client_credentials_description": "Federated client credentials allow authenticating OIDC clients without managing long-lived secrets. They leverage JWT tokens issued by third-party authorities for client assertions, e.g. workload identity tokens.", + "add_federated_client_credential": "Add Federated Client Credential", + "add_another_federated_client_credential": "Add another federated client credential", + "oidc_allowed_group_count": "Allowed Group Count", + "unrestricted": "Unrestricted", + "show_advanced_options": "Show Advanced Options", + "hide_advanced_options": "Hide Advanced Options", + "oidc_data_preview": "OIDC Data Preview", + "preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users", + "id_token": "ID Token", + "access_token": "Access Token", + "userinfo": "Userinfo", + "id_token_payload": "ID Token Payload", + "access_token_payload": "Access Token Payload", + "userinfo_endpoint_response": "Userinfo Endpoint Response", + "copy": "Copy", + "no_preview_data_available": "No preview data available", + "copy_all": "Copy All", + "preview": "Preview", + "preview_for_user": "Preview for {name}", + "preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user", + "show": "Show", + "select_an_option": "Select an option", + "select_user": "Select User", + "error": "Error", + "select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.", + "accent_color": "Accent Color", + "custom_accent_color": "Custom Accent Color", + "custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).", + "color_value": "Color Value", + "apply": "Apply", + "signup_token": "Signup Token", + "create_a_signup_token_to_allow_new_user_registration": "Create a signup token to allow new user registration.", + "usage_limit": "Usage Limit", + "number_of_times_token_can_be_used": "Number of times the signup token can be used.", + "expires": "Expires", + "signup": "Sign Up", + "user_creation": "User Creation", + "configure_user_creation": "Manage user creation settings, including signup methods and default permissions for new users.", + "user_creation_groups_description": "Assign these groups automatically to new users upon signup.", + "user_creation_claims_description": "Assign these custom claims automatically to new users upon signup.", + "user_creation_updated_successfully": "User creation settings updated successfully.", + "signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.", + "signup_requires_valid_token": "A valid signup token is required to create an account", + "validating_signup_token": "Validating signup token", + "go_to_login": "Go to login", + "signup_to_appname": "Sign Up to {appName}", + "create_your_account_to_get_started": "Create your account to get started.", + "initial_account_creation_description": "Please create your account to get started. You will be able to set up a passkey later.", + "setup_your_passkey": "Set up your passkey", + "create_a_passkey_to_securely_access_your_account": "Create a passkey to securely access your account. This will be your primary way to sign in.", + "skip_for_now": "Skip for now", + "account_created": "Account Created", + "enable_user_signups": "Enable User Signups", + "enable_user_signups_description": "Decide how users can sign up for new accounts in Pocket ID.", + "user_signups_are_disabled": "User signups are currently disabled", + "create_signup_token": "Create Signup Token", + "view_active_signup_tokens": "View Active Signup Tokens", + "manage_signup_tokens": "Manage Signup Tokens", + "view_and_manage_active_signup_tokens": "View and manage active signup tokens.", + "signup_token_deleted_successfully": "Signup token deleted successfully.", + "expired": "Expired", + "used_up": "Used Up", + "active": "Active", + "usage": "Usage", + "created": "Created", + "token": "Token", + "loading": "Loading", + "delete_signup_token": "Delete Signup Token", + "are_you_sure_you_want_to_delete_this_signup_token": "Are you sure you want to delete this signup token? This action cannot be undone.", + "signup_with_token": "Signup with token", + "signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.", + "signup_open": "Open Signup", + "signup_open_description": "Anyone can create a new account without restrictions.", + "of": "of", + "skip_passkey_setup": "Skip Passkey Setup", + "skip_passkey_setup_description": "It's highly recommended to set up a passkey because without one, you will be locked out of your account as soon as the session expires.", + "my_apps": "My Apps", + "no_apps_available": "No apps available", + "contact_your_administrator_for_app_access": "Contact your administrator to get access to applications.", + "launch": "Launch", + "client_launch_url": "Client Launch URL", + "client_launch_url_description": "The URL that will be opened when a user launches the app from the My Apps page.", + "client_name_description": "The name of the client that shows in the Pocket ID UI.", + "revoke_access": "Revoke Access", + "revoke_access_description": "Revoke access to {clientName}. {clientName} will no longer be able to access your account information.", + "revoke_access_successful": "The access to {clientName} has been successfully revoked.", + "last_signed_in_ago": "Last signed in {time} ago", + "invalid_client_id": "Client ID can only contain letters, numbers, underscores, and hyphens", + "custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.", + "generated": "Generated", + "administration": "Administration", + "group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).", + "display_name_attribute": "Display Name Attribute", + "display_name": "Display Name", + "configure_application_images": "Configure Application Images", + "ui_config_disabled_info_title": "UI Configuration Disabled", + "ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.", + "logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at Selfh.st Icons or Dashboard Icons.", + "invalid_url": "Invalid URL", + "require_user_email": "Require Email Address", + "require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address.", + "view": "View", + "toggle_columns": "Toggle columns", + "locale": "Locale", + "ldap_id": "LDAP ID", + "reauthentication": "Re-authentication", + "clear_filters": "Clear Filters", + "default_profile_picture": "Default Profile Picture", + "light": "Light", + "dark": "Dark", + "system": "System", + "signup_token_user_groups_description": "Automatically assign these groups to users who sign up using this token.", + "allowed_oidc_clients": "Allowed OIDC Clients", + "allowed_oidc_clients_description": "Select the OIDC clients that members of this user group are allowed to sign in to.", + "unrestrict_oidc_client": "Unrestrict {clientName}", + "confirm_unrestrict_oidc_client_description": "Are you sure you want to unrestrict the OIDC client {clientName}? This will remove all group assignments for this client and any user will be able to sign in.", + "allowed_oidc_clients_updated_successfully": "Allowed OIDC clients updated successfully", + "yes": "Yes", + "no": "No", + "restricted": "Restricted", + "scim_provisioning": "SCIM Provisioning", + "scim_provisioning_description": "SCIM provisioning allows you to automatically provision and deprovision users and groups from your OIDC client. Learn more in the docs.", + "scim_endpoint": "SCIM Endpoint", + "scim_token": "SCIM Token", + "last_successful_sync_at": "Last successful sync: {time}", + "scim_configuration_updated_successfully": "SCIM configuration updated successfully.", + "scim_enabled_successfully": "SCIM enabled successfully.", + "scim_disabled_successfully": "SCIM disabled successfully.", + "disable_scim_provisioning": "Disable SCIM Provisioning", + "disable_scim_provisioning_confirm_description": "Are you sure you want to disable SCIM provisioning for {clientName}? This will stop all automatic user and group provisioning and deprovisioning.", + "scim_sync_failed": "SCIM sync failed. Check the server logs for more information.", + "scim_sync_successful": "The SCIM sync has been completed successfully.", + "save_and_sync": "Save and Sync", + "scim_save_changes_description": "You have to save the changes before starting a SCIM sync. Do you want to save now?", + "scopes": "Scopes", + "issuer_url": "Issuer URL", + "smtp_field_required_when_other_provided": "Required when any SMTP setting is provided", + "smtp_field_required_when_email_enabled": "Required when email notifications are enabled", + "renew": "Renew", + "renew_api_key": "Renew API Key", + "renew_api_key_description": "Renewing the API key will generate a new key. Make sure to update any integrations using this key.", + "api_key_renewed": "API key renewed", + "app_config_home_page": "Home Page", + "app_config_home_page_description": "The page users are redirected to after signing in.", + "email_verification_warning": "Verify your email address", + "email_verification_warning_description": "Your email address is not verified yet. Please verify it as soon as possible.", + "email_verification": "Email Verification", + "email_verification_description": "Send a verification email to users when they sign up or change their email address.", + "email_verification_success_title": "Email Verified Successfully", + "email_verification_success_description": "Your email address has been verified successfully.", + "email_verification_error_title": "Email Verification Failed", + "mark_as_unverified": "Mark as unverified", + "mark_as_verified": "Mark as verified", + "email_verification_sent": "Verification email sent successfully.", + "emails_verified_by_default": "Emails verified by default", + "emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed." +} diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index 19cb99d3..958da44c 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Voer de code in die in de vorige stap werd getoond.", "authorize": "Autoriseren", "federated_client_credentials": "Federatieve clientreferenties", - "federated_client_credentials_description": "Met federatieve clientreferenties kun je OIDC-clients verifiëren met JWT-tokens die zijn uitgegeven door andere instanties.", "add_federated_client_credential": "Federatieve clientreferenties toevoegen", "add_another_federated_client_credential": "Voeg nog een federatieve clientreferentie toe", "oidc_allowed_group_count": "Aantal groepen met toegang", diff --git a/frontend/messages/no.json b/frontend/messages/no.json index 1c131f38..320291e5 100644 --- a/frontend/messages/no.json +++ b/frontend/messages/no.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.", "authorize": "Authorize", "federated_client_credentials": "Federated Client Credentials", - "federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.", "add_federated_client_credential": "Add Federated Client Credential", "add_another_federated_client_credential": "Add another federated client credential", "oidc_allowed_group_count": "Allowed Group Count", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index ee6a0148..be63509c 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Wprowadź kod wyświetlony w poprzednim kroku.", "authorize": "Autoryzuj", "federated_client_credentials": "Połączone poświadczenia klienta", - "federated_client_credentials_description": "Korzystając z połączonych poświadczeń klienta, możecie uwierzytelnić klientów OIDC za pomocą tokenów JWT wydanych przez zewnętrzne organy.", "add_federated_client_credential": "Dodaj poświadczenia klienta federacyjnego", "add_another_federated_client_credential": "Dodaj kolejne poświadczenia klienta federacyjnego", "oidc_allowed_group_count": "Dopuszczalna liczba grup", diff --git a/frontend/messages/pt-BR.json b/frontend/messages/pt-BR.json index 1b94665b..3d28baec 100644 --- a/frontend/messages/pt-BR.json +++ b/frontend/messages/pt-BR.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Digite o código que apareceu na etapa anterior.", "authorize": "Autorizar", "federated_client_credentials": "Credenciais de Cliente Federadas", - "federated_client_credentials_description": "Ao utilizar credenciais de cliente federadas, é possível autenticar clientes OIDC usando tokens JWT emitidos por autoridades de terceiros.", "add_federated_client_credential": "Adicionar credencial de cliente federado", "add_another_federated_client_credential": "Adicionar outra credencial de cliente federado", "oidc_allowed_group_count": "Total de grupos permitidos", diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json new file mode 100644 index 00000000..e89938b8 --- /dev/null +++ b/frontend/messages/pt.json @@ -0,0 +1,525 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "my_account": "My Account", + "logout": "Logout", + "confirm": "Confirm", + "docs": "Docs", + "key": "Key", + "value": "Value", + "remove_custom_claim": "Remove custom claim", + "add_custom_claim": "Add custom claim", + "add_another": "Add another", + "select_a_date": "Select a date", + "select_file": "Select File", + "profile_picture": "Profile Picture", + "profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.", + "click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.", + "image_should_be_in_format": "The image should be in PNG, JPEG or WEBP format.", + "items_per_page": "Items per page", + "no_items_found": "No items found", + "select_items": "Select items...", + "search": "Search...", + "expand_card": "Expand card", + "copied": "Copied", + "click_to_copy": "Click to copy", + "something_went_wrong": "Something went wrong", + "go_back_to_home": "Go back to home", + "alternative_sign_in_methods": "Alternative Sign In Methods", + "login_background": "Login background", + "logo": "Logo", + "login_code": "Login Code", + "create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.", + "one_hour": "1 hour", + "twelve_hours": "12 hours", + "one_day": "1 day", + "one_week": "1 week", + "one_month": "1 month", + "expiration": "Expiration", + "generate_code": "Generate Code", + "name": "Name", + "browser_unsupported": "Browser unsupported", + "this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please use an alternative sign in method.", + "an_unknown_error_occurred": "An unknown error occurred", + "authentication_process_was_aborted": "The authentication process was aborted", + "error_occurred_with_authenticator": "An error occurred with the authenticator", + "authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials", + "authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys", + "passkey_was_previously_registered": "This passkey was previously registered", + "authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms", + "webauthn_error_invalid_rp_id": "The configured relying party ID is invalid.", + "webauthn_error_invalid_domain": "The configured domain is invalid.", + "contact_administrator_to_fix": "Contact your administrator to fix this issue.", + "webauthn_operation_not_allowed_or_timed_out": "The operation was not allowed or timed out", + "webauthn_not_supported_by_browser": "Passkeys are not supported by this browser. Please use an alternative sign in method.", + "critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.", + "sign_in_to": "Sign in to {name}", + "client_not_found": "Client not found", + "client_wants_to_access_the_following_information": "{client} wants to access the following information:", + "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to {client} with your {appName} account?", + "email": "Email", + "view_your_email_address": "View your email address", + "profile": "Profile", + "view_your_profile_information": "View your profile information", + "groups": "Groups", + "view_the_groups_you_are_a_member_of": "View the groups you are a member of", + "cancel": "Cancel", + "sign_in": "Sign in", + "try_again": "Try again", + "client_logo": "Client Logo", + "sign_out": "Sign out", + "do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of {appName} with the account {username}?", + "sign_in_to_appname": "Sign in to {appName}", + "please_try_to_sign_in_again": "Please try to sign in again.", + "authenticate_with_passkey_to_access_account": "Authenticate yourself with your passkey to access your account.", + "authenticate": "Authenticate", + "please_try_again": "Please try again.", + "continue": "Continue", + "alternative_sign_in": "Alternative Sign In", + "if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you don't have access to your passkey, you can sign in using one of the following methods.", + "use_your_passkey_instead": "Use your passkey instead?", + "email_login": "Email Login", + "enter_a_login_code_to_sign_in": "Enter a login code to sign in.", + "sign_in_with_login_code": "Sign in with login code", + "request_a_login_code_via_email": "Request a login code via email.", + "go_back": "Go back", + "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.", + "enter_code": "Enter code", + "enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.", + "your_email": "Your email", + "submit": "Submit", + "enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.", + "code": "Code", + "invalid_redirect_url": "Invalid redirect URL", + "audit_log": "Audit Log", + "users": "Users", + "user_groups": "User Groups", + "oidc_clients": "OIDC Clients", + "api_keys": "API Keys", + "application_configuration": "Application Configuration", + "settings": "Settings", + "update_pocket_id": "Update Pocket ID", + "powered_by": "Powered by", + "see_your_recent_account_activities": "See your account activities within the configured retention period.", + "time": "Time", + "event": "Event", + "approximate_location": "Approximate Location", + "ip_address": "IP Address", + "device": "Device", + "client": "Client", + "unknown": "Unknown", + "account_details_updated_successfully": "Account details updated successfully", + "profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.", + "account_settings": "Account Settings", + "passkey_missing": "Passkey missing", + "please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Please add a passkey to prevent losing access to your account.", + "single_passkey_configured": "Single Passkey Configured", + "it_is_recommended_to_add_more_than_one_passkey": "It is recommended to add more than one passkey to avoid losing access to your account.", + "account_details": "Account Details", + "passkeys": "Passkeys", + "manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.", + "add_passkey": "Add Passkey", + "create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.", + "create": "Create", + "first_name": "First name", + "last_name": "Last name", + "username": "Username", + "save": "Save", + "username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols", + "username_must_start_with": "Username must start with an alphanumeric character", + "username_must_end_with": "Username must end with an alphanumeric character", + "sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.", + "or_visit": "or visit", + "added_on": "Added on", + "rename": "Rename", + "delete": "Delete", + "are_you_sure_you_want_to_delete_this_passkey": "Are you sure you want to delete this passkey?", + "passkey_deleted_successfully": "Passkey deleted successfully", + "delete_passkey_name": "Delete {passkeyName}", + "passkey_name_updated_successfully": "Passkey name updated successfully", + "name_passkey": "Name Passkey", + "name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.", + "create_api_key": "Create API Key", + "add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access to the Pocket ID API.", + "add_api_key": "Add API Key", + "manage_api_keys": "Manage API Keys", + "api_key_created": "API Key Created", + "for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.", + "description": "Description", + "api_key": "API Key", + "close": "Close", + "name_to_identify_this_api_key": "Name to identify this API key.", + "expires_at": "Expires At", + "when_this_api_key_will_expire": "When this API key will expire.", + "optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.", + "expiration_date_must_be_in_the_future": "Expiration date must be in the future", + "revoke_api_key": "Revoke API Key", + "never": "Never", + "revoke": "Revoke", + "api_key_revoked_successfully": "API key revoked successfully", + "are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.", + "last_used": "Last Used", + "actions": "Actions", + "images_updated_successfully": "Images updated successfully. It may take a few minutes to update.", + "general": "General", + "configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.", + "ldap": "LDAP", + "configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.", + "images": "Images", + "update": "Update", + "email_configuration_updated_successfully": "Email configuration updated successfully", + "save_changes_question": "Save changes?", + "you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?", + "save_and_send": "Save and send", + "test_email_sent_successfully": "Test email sent successfully to your email address.", + "failed_to_send_test_email": "Failed to send test email. Check the server logs for more information.", + "smtp_configuration": "SMTP Configuration", + "smtp_host": "SMTP Host", + "smtp_port": "SMTP Port", + "smtp_user": "SMTP User", + "smtp_password": "SMTP Password", + "smtp_from": "SMTP From", + "smtp_tls_option": "SMTP TLS Option", + "email_tls_option": "Email TLS Option", + "skip_certificate_verification": "Skip Certificate Verification", + "this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.", + "enabled_emails": "Enabled Emails", + "email_login_notification": "Email Login Notification", + "send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.", + "emai_login_code_requested_by_user": "Email Login Code Requested by User", + "allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.", + "email_login_code_from_admin": "Email Login Code from Admin", + "allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.", + "send_test_email": "Send test email", + "application_configuration_updated_successfully": "Application configuration updated successfully", + "application_name": "Application Name", + "session_duration": "Session Duration", + "the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "The duration of a session in minutes before the user has to sign in again.", + "enable_self_account_editing": "Enable Self-Account Editing", + "whether_the_users_should_be_able_to_edit_their_own_account_details": "Whether the users should be able to edit their own account details.", + "ldap_configuration_updated_successfully": "LDAP configuration updated successfully", + "ldap_disabled_successfully": "LDAP disabled successfully", + "ldap_sync_finished": "LDAP sync finished", + "client_configuration": "Client Configuration", + "ldap_url": "LDAP URL", + "ldap_bind_dn": "LDAP Bind DN", + "ldap_bind_password": "LDAP Bind Password", + "ldap_base_dn": "LDAP Base DN", + "user_search_filter": "User Search Filter", + "the_search_filter_to_use_to_search_or_sync_users": "The Search filter to use to search/sync users.", + "groups_search_filter": "Groups Search Filter", + "the_search_filter_to_use_to_search_or_sync_groups": "The Search filter to use to search/sync groups.", + "attribute_mapping": "Attribute Mapping", + "user_unique_identifier_attribute": "User Unique Identifier Attribute", + "the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.", + "username_attribute": "Username Attribute", + "user_mail_attribute": "User Mail Attribute", + "user_first_name_attribute": "User First Name Attribute", + "user_last_name_attribute": "User Last Name Attribute", + "user_profile_picture_attribute": "User Profile Picture Attribute", + "the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "The value of this attribute can either be a URL, a binary or a base64 encoded image.", + "group_members_attribute": "Group Members Attribute", + "the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.", + "group_unique_identifier_attribute": "Group Unique Identifier Attribute", + "group_rdn_attribute": "Group RDN Attribute (in DN)", + "admin_group_name": "Admin Group Name", + "members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.", + "disable": "Disable", + "sync_now": "Sync now", + "enable": "Enable", + "user_created_successfully": "User created successfully", + "create_user": "Create User", + "add_a_new_user_to_appname": "Add a new user to {appName}", + "add_user": "Add User", + "manage_users": "Manage Users", + "admin_privileges": "Admin Privileges", + "admins_have_full_access_to_the_admin_panel": "Admins have full access to the admin panel.", + "delete_firstname_lastname": "Delete {firstName} {lastName}", + "are_you_sure_you_want_to_delete_this_user": "Are you sure you want to delete this user?", + "user_deleted_successfully": "User deleted successfully", + "role": "Role", + "source": "Source", + "admin": "Admin", + "user": "User", + "local": "Local", + "toggle_menu": "Toggle menu", + "edit": "Edit", + "user_groups_updated_successfully": "User groups updated successfully", + "user_updated_successfully": "User updated successfully", + "custom_claims_updated_successfully": "Custom claims updated successfully", + "back": "Back", + "user_details_firstname_lastname": "User Details {firstName} {lastName}", + "manage_which_groups_this_user_belongs_to": "Manage which groups this user belongs to.", + "custom_claims": "Custom Claims", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested.", + "user_group_created_successfully": "User group created successfully", + "create_user_group": "Create User Group", + "create_a_new_group_that_can_be_assigned_to_users": "Create a new group that can be assigned to users.", + "add_group": "Add Group", + "manage_user_groups": "Manage User Groups", + "friendly_name": "Friendly Name", + "name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI", + "name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim", + "delete_name": "Delete {name}", + "are_you_sure_you_want_to_delete_this_user_group": "Are you sure you want to delete this user group?", + "user_group_deleted_successfully": "User group deleted successfully", + "user_count": "User Count", + "user_group_updated_successfully": "User group updated successfully", + "users_updated_successfully": "Users updated successfully", + "user_group_details_name": "User Group Details {name}", + "assign_users_to_this_group": "Assign users to this group.", + "custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts.", + "oidc_client_created_successfully": "OIDC client created successfully", + "create_oidc_client": "Create OIDC Client", + "add_a_new_oidc_client_to_appname": "Add a new OIDC client to {appName}.", + "add_oidc_client": "Add OIDC Client", + "manage_oidc_clients": "Manage OIDC Clients", + "one_time_link": "One Time Link", + "use_this_link_to_sign_in_once": "Use this link to sign in once. This is needed for users who haven't added a passkey yet or have lost it.", + "add": "Add", + "callback_urls": "Callback URLs", + "logout_callback_urls": "Logout Callback URLs", + "public_client": "Public Client", + "public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.", + "pkce": "PKCE", + "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.", + "requires_reauthentication": "Requires Re-Authentication", + "requires_users_to_authenticate_again_on_each_authorization": "Requires users to authenticate again on each authorization, even if already signed in", + "name_logo": "{name} logo", + "change_logo": "Change Logo", + "upload_logo": "Upload Logo", + "remove_logo": "Remove Logo", + "are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?", + "oidc_client_deleted_successfully": "OIDC client deleted successfully", + "authorization_url": "Authorization URL", + "oidc_discovery_url": "OIDC Discovery URL", + "token_url": "Token URL", + "userinfo_url": "Userinfo URL", + "logout_url": "Logout URL", + "certificate_url": "Certificate URL", + "enabled": "Enabled", + "disabled": "Disabled", + "oidc_client_updated_successfully": "OIDC client updated successfully", + "create_new_client_secret": "Create new client secret", + "are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.", + "generate": "Generate", + "new_client_secret_created_successfully": "New client secret created successfully", + "oidc_client_name": "OIDC Client {name}", + "client_id": "Client ID", + "client_secret": "Client secret", + "show_more_details": "Show more details", + "allowed_user_groups": "Allowed User Groups", + "allowed_user_groups_description": "Select the user groups whose members are allowed to sign in to this client.", + "allowed_user_groups_status_unrestricted_description": "No user group restrictions are applied. Any user can sign in to this client.", + "unrestrict": "Unrestrict", + "restrict": "Restrict", + "user_groups_restriction_updated_successfully": "User groups restriction updated successfully", + "allowed_user_groups_updated_successfully": "Allowed user groups updated successfully", + "favicon": "Favicon", + "light_mode_logo": "Light Mode Logo", + "dark_mode_logo": "Dark Mode Logo", + "email_logo": "Email Logo", + "background_image": "Background Image", + "language": "Language", + "reset_profile_picture_question": "Reset profile picture?", + "this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "This will remove the uploaded image and reset the profile picture to default. Do you want to continue?", + "reset": "Reset", + "reset_to_default": "Reset to default", + "profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.", + "select_the_language_you_want_to_use": "Select the language you want to use. Please note that some text may be automatically translated and could be inaccurate.", + "contribute_to_translation": "If you find an issue you're welcome to contribute to the translation on Crowdin.", + "personal": "Personal", + "global": "Global", + "all_users": "All Users", + "all_events": "All Events", + "all_clients": "All Clients", + "all_locations": "All Locations", + "global_audit_log": "Global Audit Log", + "see_all_recent_account_activities": "View the account activities of all users during the set retention period.", + "token_sign_in": "Token Sign In", + "client_authorization": "Client Authorization", + "new_client_authorization": "New Client Authorization", + "device_code_authorization": "Device Code Authorization", + "new_device_code_authorization": "New Device Code Authorization", + "passkey_added": "Passkey Added", + "passkey_removed": "Passkey Removed", + "disable_animations": "Disable Animations", + "turn_off_ui_animations": "Turn off animations throughout the UI.", + "user_disabled": "Account Disabled", + "disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.", + "user_disabled_successfully": "User has been disabled successfully.", + "user_enabled_successfully": "User has been enabled successfully.", + "status": "Status", + "disable_firstname_lastname": "Disable {firstName} {lastName}", + "are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.", + "ldap_soft_delete_users": "Keep disabled users from LDAP.", + "ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.", + "login_code_email_success": "The login code has been sent to the user.", + "send_email": "Send Email", + "show_code": "Show Code", + "callback_url_description": "URL(s) provided by your client. Will be automatically added if left blank. Wildcards are supported.", + "logout_callback_url_description": "URL(s) provided by your client for logout. Wildcards are supported.", + "api_key_expiration": "API Key Expiration", + "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.", + "authorize_device": "Authorize Device", + "the_device_has_been_authorized": "The device has been authorized.", + "enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.", + "authorize": "Authorize", + "federated_client_credentials": "Federated Client Credentials", + "federated_client_credentials_description": "Federated client credentials allow authenticating OIDC clients without managing long-lived secrets. They leverage JWT tokens issued by third-party authorities for client assertions, e.g. workload identity tokens.", + "add_federated_client_credential": "Add Federated Client Credential", + "add_another_federated_client_credential": "Add another federated client credential", + "oidc_allowed_group_count": "Allowed Group Count", + "unrestricted": "Unrestricted", + "show_advanced_options": "Show Advanced Options", + "hide_advanced_options": "Hide Advanced Options", + "oidc_data_preview": "OIDC Data Preview", + "preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users", + "id_token": "ID Token", + "access_token": "Access Token", + "userinfo": "Userinfo", + "id_token_payload": "ID Token Payload", + "access_token_payload": "Access Token Payload", + "userinfo_endpoint_response": "Userinfo Endpoint Response", + "copy": "Copy", + "no_preview_data_available": "No preview data available", + "copy_all": "Copy All", + "preview": "Preview", + "preview_for_user": "Preview for {name}", + "preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user", + "show": "Show", + "select_an_option": "Select an option", + "select_user": "Select User", + "error": "Error", + "select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.", + "accent_color": "Accent Color", + "custom_accent_color": "Custom Accent Color", + "custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).", + "color_value": "Color Value", + "apply": "Apply", + "signup_token": "Signup Token", + "create_a_signup_token_to_allow_new_user_registration": "Create a signup token to allow new user registration.", + "usage_limit": "Usage Limit", + "number_of_times_token_can_be_used": "Number of times the signup token can be used.", + "expires": "Expires", + "signup": "Sign Up", + "user_creation": "User Creation", + "configure_user_creation": "Manage user creation settings, including signup methods and default permissions for new users.", + "user_creation_groups_description": "Assign these groups automatically to new users upon signup.", + "user_creation_claims_description": "Assign these custom claims automatically to new users upon signup.", + "user_creation_updated_successfully": "User creation settings updated successfully.", + "signup_disabled_description": "User signups are completely disabled. Only administrators can create new user accounts.", + "signup_requires_valid_token": "A valid signup token is required to create an account", + "validating_signup_token": "Validating signup token", + "go_to_login": "Go to login", + "signup_to_appname": "Sign Up to {appName}", + "create_your_account_to_get_started": "Create your account to get started.", + "initial_account_creation_description": "Please create your account to get started. You will be able to set up a passkey later.", + "setup_your_passkey": "Set up your passkey", + "create_a_passkey_to_securely_access_your_account": "Create a passkey to securely access your account. This will be your primary way to sign in.", + "skip_for_now": "Skip for now", + "account_created": "Account Created", + "enable_user_signups": "Enable User Signups", + "enable_user_signups_description": "Decide how users can sign up for new accounts in Pocket ID.", + "user_signups_are_disabled": "User signups are currently disabled", + "create_signup_token": "Create Signup Token", + "view_active_signup_tokens": "View Active Signup Tokens", + "manage_signup_tokens": "Manage Signup Tokens", + "view_and_manage_active_signup_tokens": "View and manage active signup tokens.", + "signup_token_deleted_successfully": "Signup token deleted successfully.", + "expired": "Expired", + "used_up": "Used Up", + "active": "Active", + "usage": "Usage", + "created": "Created", + "token": "Token", + "loading": "Loading", + "delete_signup_token": "Delete Signup Token", + "are_you_sure_you_want_to_delete_this_signup_token": "Are you sure you want to delete this signup token? This action cannot be undone.", + "signup_with_token": "Signup with token", + "signup_with_token_description": "Users can only sign up using a valid signup token created by an administrator.", + "signup_open": "Open Signup", + "signup_open_description": "Anyone can create a new account without restrictions.", + "of": "of", + "skip_passkey_setup": "Skip Passkey Setup", + "skip_passkey_setup_description": "It's highly recommended to set up a passkey because without one, you will be locked out of your account as soon as the session expires.", + "my_apps": "My Apps", + "no_apps_available": "No apps available", + "contact_your_administrator_for_app_access": "Contact your administrator to get access to applications.", + "launch": "Launch", + "client_launch_url": "Client Launch URL", + "client_launch_url_description": "The URL that will be opened when a user launches the app from the My Apps page.", + "client_name_description": "The name of the client that shows in the Pocket ID UI.", + "revoke_access": "Revoke Access", + "revoke_access_description": "Revoke access to {clientName}. {clientName} will no longer be able to access your account information.", + "revoke_access_successful": "The access to {clientName} has been successfully revoked.", + "last_signed_in_ago": "Last signed in {time} ago", + "invalid_client_id": "Client ID can only contain letters, numbers, underscores, and hyphens", + "custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.", + "generated": "Generated", + "administration": "Administration", + "group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).", + "display_name_attribute": "Display Name Attribute", + "display_name": "Display Name", + "configure_application_images": "Configure Application Images", + "ui_config_disabled_info_title": "UI Configuration Disabled", + "ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.", + "logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at Selfh.st Icons or Dashboard Icons.", + "invalid_url": "Invalid URL", + "require_user_email": "Require Email Address", + "require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address.", + "view": "View", + "toggle_columns": "Toggle columns", + "locale": "Locale", + "ldap_id": "LDAP ID", + "reauthentication": "Re-authentication", + "clear_filters": "Clear Filters", + "default_profile_picture": "Default Profile Picture", + "light": "Light", + "dark": "Dark", + "system": "System", + "signup_token_user_groups_description": "Automatically assign these groups to users who sign up using this token.", + "allowed_oidc_clients": "Allowed OIDC Clients", + "allowed_oidc_clients_description": "Select the OIDC clients that members of this user group are allowed to sign in to.", + "unrestrict_oidc_client": "Unrestrict {clientName}", + "confirm_unrestrict_oidc_client_description": "Are you sure you want to unrestrict the OIDC client {clientName}? This will remove all group assignments for this client and any user will be able to sign in.", + "allowed_oidc_clients_updated_successfully": "Allowed OIDC clients updated successfully", + "yes": "Yes", + "no": "No", + "restricted": "Restricted", + "scim_provisioning": "SCIM Provisioning", + "scim_provisioning_description": "SCIM provisioning allows you to automatically provision and deprovision users and groups from your OIDC client. Learn more in the docs.", + "scim_endpoint": "SCIM Endpoint", + "scim_token": "SCIM Token", + "last_successful_sync_at": "Last successful sync: {time}", + "scim_configuration_updated_successfully": "SCIM configuration updated successfully.", + "scim_enabled_successfully": "SCIM enabled successfully.", + "scim_disabled_successfully": "SCIM disabled successfully.", + "disable_scim_provisioning": "Disable SCIM Provisioning", + "disable_scim_provisioning_confirm_description": "Are you sure you want to disable SCIM provisioning for {clientName}? This will stop all automatic user and group provisioning and deprovisioning.", + "scim_sync_failed": "SCIM sync failed. Check the server logs for more information.", + "scim_sync_successful": "The SCIM sync has been completed successfully.", + "save_and_sync": "Save and Sync", + "scim_save_changes_description": "You have to save the changes before starting a SCIM sync. Do you want to save now?", + "scopes": "Scopes", + "issuer_url": "Issuer URL", + "smtp_field_required_when_other_provided": "Required when any SMTP setting is provided", + "smtp_field_required_when_email_enabled": "Required when email notifications are enabled", + "renew": "Renew", + "renew_api_key": "Renew API Key", + "renew_api_key_description": "Renewing the API key will generate a new key. Make sure to update any integrations using this key.", + "api_key_renewed": "API key renewed", + "app_config_home_page": "Home Page", + "app_config_home_page_description": "The page users are redirected to after signing in.", + "email_verification_warning": "Verify your email address", + "email_verification_warning_description": "Your email address is not verified yet. Please verify it as soon as possible.", + "email_verification": "Email Verification", + "email_verification_description": "Send a verification email to users when they sign up or change their email address.", + "email_verification_success_title": "Email Verified Successfully", + "email_verification_success_description": "Your email address has been verified successfully.", + "email_verification_error_title": "Email Verification Failed", + "mark_as_unverified": "Mark as unverified", + "mark_as_verified": "Mark as verified", + "email_verification_sent": "Verification email sent successfully.", + "emails_verified_by_default": "Emails verified by default", + "emails_verified_by_default_description": "When enabled, users' email addresses will be marked as verified by default upon signup or when their email address is changed." +} diff --git a/frontend/messages/ru.json b/frontend/messages/ru.json index 3f2239fa..7774a94e 100644 --- a/frontend/messages/ru.json +++ b/frontend/messages/ru.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.", "authorize": "Авторизовать", "federated_client_credentials": "Федеративные учетные данные клиента", - "federated_client_credentials_description": "Используя федеративные учетные данные клиента, вы можете аутентифицировать клиентов OIDC с помощью токенов JWT, выпущенных сторонними поставщиками удостоверений.", "add_federated_client_credential": "Добавить федеративные учетные данные клиента", "add_another_federated_client_credential": "Добавить другие федеративные учетные данные клиента", "oidc_allowed_group_count": "Число разрешенных групп", diff --git a/frontend/messages/sv.json b/frontend/messages/sv.json index c45fdf43..2a46b86f 100644 --- a/frontend/messages/sv.json +++ b/frontend/messages/sv.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Ange koden som visades i föregående steg.", "authorize": "Godkänn", "federated_client_credentials": "Federerade klientuppgifter", - "federated_client_credentials_description": "Med hjälp av federerade klientuppgifter kan du autentisera OIDC-klienter med JWT-tokens som utfärdats av externa auktoriteter.", "add_federated_client_credential": "Lägg till federerad klientuppgift", "add_another_federated_client_credential": "Lägg till ytterligare en federerad klientuppgift", "oidc_allowed_group_count": "Tillåtet antal grupper", diff --git a/frontend/messages/tr.json b/frontend/messages/tr.json index dc7b5985..608939db 100644 --- a/frontend/messages/tr.json +++ b/frontend/messages/tr.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Önceki adımda görüntülenen kodu girin.", "authorize": "Yetkilendir", "federated_client_credentials": "Birleştirilmiş İstemci Kimlik Bilgileri", - "federated_client_credentials_description": "Birleşik istemci kimlik bilgilerini kullanarak, üçüncü taraf otoriteleri tarafından verilen JWT token'ları kullanarak OIDC istemcilerinin kimliklerini doğrulayabilirsiniz.", "add_federated_client_credential": "Birleştirilmiş İstemci Kimlik Bilgisi Ekle", "add_another_federated_client_credential": "Başka bir birleştirilmiş istemci kimlik bilgisi ekle", "oidc_allowed_group_count": "İzin Verilen Grup Sayısı", diff --git a/frontend/messages/uk.json b/frontend/messages/uk.json index 147f46f7..06b548fe 100644 --- a/frontend/messages/uk.json +++ b/frontend/messages/uk.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Введіть код, який було показано на попередньому кроці.", "authorize": "Авторизувати", "federated_client_credentials": "Федеративні облікові дані клієнта", - "federated_client_credentials_description": "За допомогою федеративних облікових даних клієнта ви можете автентифікувати клієнтів OIDC за допомогою токенів JWT, виданих третіми сторонами.", "add_federated_client_credential": "Додати федеративний обліковий запис клієнта", "add_another_federated_client_credential": "Додати ще один федеративний обліковий запис клієнта", "oidc_allowed_group_count": "Кількість дозволених груп", @@ -457,69 +456,69 @@ "custom_client_id_description": "Встановіть власний ідентифікатор клієнта, якщо це вимагається вашим застосунком. В іншому випадку залиште поле порожнім, щоб згенерувати випадковий.", "generated": "Створено", "administration": "Адміністрування", - "group_rdn_attribute_description": "Атрибут, що використовується в розрізнювальному імені групи (DN).", + "group_rdn_attribute_description": "Атрибут, який використовується в DN (Distinguished Name) групи.", "display_name_attribute": "Атрибут імені для відображення", "display_name": "Ім'я для відображення", "configure_application_images": "Налаштування зображень застосунку", - "ui_config_disabled_info_title": "Конфігурація інтерфейсу користувача вимкнена", - "ui_config_disabled_info_description": "Конфігурація інтерфейсу користувача вимкнена, оскільки налаштування конфігурації програми керуються через змінні середовища. Деякі налаштування можуть бути недоступними для редагування.", + "ui_config_disabled_info_title": "Налаштування через UI вимкнено", + "ui_config_disabled_info_description": "Налаштування через UI вимкнено, оскільки параметри конфігурації застосунку керуються через змінні середовища. Деякі налаштування можуть бути недоступні для редагування.", "logo_from_url_description": "Вставте пряму URL-адресу зображення (svg, png, webp). Знайдіть іконки на Selfh.st Icons або Dashboard Icons.", "invalid_url": "Недійсна URL-адреса", "require_user_email": "Потрібна адреса електронної пошти", - "require_user_email_description": "Вимагає від користувачів наявність адреси електронної пошти. Якщо ця опція вимкнена, користувачі без адреси електронної пошти не зможуть користуватися функціями, для яких потрібна адреса електронної пошти.", + "require_user_email_description": "Вимагає наявності електронної адреси у користувачів. Якщо вимкнено, користувачі без електронної адреси не зможуть користуватися функціями, які її вимагають.", "view": "Перегляд", - "toggle_columns": "Перемикання стовпців", - "locale": "Локаль", + "toggle_columns": "Налаштувати стовпці", + "locale": "Мова", "ldap_id": "LDAP-ідентифікатор", - "reauthentication": "Повторна аутентифікація", + "reauthentication": "Повторна автентифікація", "clear_filters": "Очистити фільтри", "default_profile_picture": "Стандартне зображення профілю", "light": "Світла", "dark": "Темна", "system": "Системна", "signup_token_user_groups_description": "Автоматично призначати ці групи користувачам, які реєструються за допомогою цього токена.", - "allowed_oidc_clients": "Дозволені клієнти OIDC", - "allowed_oidc_clients_description": "Виберіть клієнти OIDC, до яких члени цієї групи користувачів мають право входити.", + "allowed_oidc_clients": "Дозволені OIDC-клієнти", + "allowed_oidc_clients_description": "Оберіть OIDC-клієнти, до яких дозволено вхід членам цієї групи користувачів.", "unrestrict_oidc_client": "Не обмежувати {clientName}", - "confirm_unrestrict_oidc_client_description": "Ви впевнені, що хочете зняти обмеження з клієнта OIDC {clientName}? Це призведе до видалення всіх групових призначень для цього клієнта, і будь-який користувач зможе увійти в систему.", - "allowed_oidc_clients_updated_successfully": "Дозволені клієнти OIDC успішно оновлені", + "confirm_unrestrict_oidc_client_description": "Ви впевнені, що хочете зняти обмеження з OIDC-клієнта {clientName}? Це видалить усі призначення груп для цього клієнта, і будь-який користувач зможе виконати вхід.", + "allowed_oidc_clients_updated_successfully": "Дозволені OIDC-клієнти успішно оновлено", "yes": "Так", "no": "Ні", "restricted": "Обмежений", - "scim_provisioning": "Надання SCIM", - "scim_provisioning_description": "SCIM-провізінінг дозволяє автоматично надавати та скасовувати доступ користувачам і групам з вашого клієнта OIDC. Дізнайтеся більше в документації.", + "scim_provisioning": "Синхронізація SCIM", + "scim_provisioning_description": "Постачання користувачів через SCIM дозволяє автоматично додавати та видаляти користувачів і групи у вашому OIDC-клієнті. Дізнайтеся більше у документації.", "scim_endpoint": "Кінцева точка SCIM", "scim_token": "Токен SCIM", "last_successful_sync_at": "Остання успішна синхронізація: {time}", - "scim_configuration_updated_successfully": "Конфігурація SCIM успішно оновлена.", - "scim_enabled_successfully": "SCIM успішно увімкнено.", - "scim_disabled_successfully": "SCIM успішно вимкнено.", - "disable_scim_provisioning": "Вимкнути надання SCIM", - "disable_scim_provisioning_confirm_description": "Ви впевнені, що хочете вимкнути надання доступу SCIM для {clientName}? Це зупинить всі автоматичні процеси надання та скасування доступу для користувачів і груп.", + "scim_configuration_updated_successfully": "Конфігурацію SCIM успішно оновлено.", + "scim_enabled_successfully": "Синхронізація SCIM успішно увімкнено.", + "scim_disabled_successfully": "Синхронізація SCIM успішно вимкнено.", + "disable_scim_provisioning": "Вимкнути SCIM синхронізацію", + "disable_scim_provisioning_confirm_description": "Ви впевнені, що хочете вимкнути постачання користувачів через SCIM для {clientName}? Це зупинить автоматичне додавання та видалення користувачів і груп.", "scim_sync_failed": "Синхронізація SCIM не вдалася. Перевірте журнали сервера для отримання додаткової інформації.", "scim_sync_successful": "Синхронізація SCIM успішно завершена.", "save_and_sync": "Зберегти та синхронізувати", - "scim_save_changes_description": "Перед початком синхронізації SCIM необхідно зберегти зміни. Чи хочете ви зберегти зараз?", + "scim_save_changes_description": "Необхідно зберегти зміни перед запуском синхронізації SCIM. Бажаєте зберегти зараз?", "scopes": "Області застосування", "issuer_url": "URL емітента", - "smtp_field_required_when_other_provided": "Необхідно, якщо вказано будь-яке налаштування SMTP", - "smtp_field_required_when_email_enabled": "Необхідно, якщо увімкнено сповіщення електронною поштою", - "renew": "Оновити", - "renew_api_key": "Оновити API-ключ", - "renew_api_key_description": "Оновлення API-ключа призведе до створення нового ключа. Обов'язково оновіть усі інтеграції, що використовують цей ключ.", - "api_key_renewed": "API-ключ оновлено", + "smtp_field_required_when_other_provided": "Обов'язково, якщо вказано будь-який параметр SMTP", + "smtp_field_required_when_email_enabled": "Обов'язково, якщо увімкнено сповіщення електронною поштою", + "renew": "Поновити", + "renew_api_key": "Поновити API-ключ", + "renew_api_key_description": "Поновлення API-ключа згенерує новий ключ. Переконайтеся, що ви оновили всі інтеграції, які його використовують.", + "api_key_renewed": "API-ключ поновлено", "app_config_home_page": "Головна сторінка", - "app_config_home_page_description": "Сторінка, на яку перенаправляють користувачів після входу в систему.", + "app_config_home_page_description": "Сторінка, на яку користувачі перенаправляються після входу.", "email_verification_warning": "Підтвердьте свою адресу електронної пошти", "email_verification_warning_description": "Ваша електронна адреса ще не підтверджена. Будь ласка, підтвердьте її якомога швидше.", - "email_verification": "Перевірка електронної адреси", - "email_verification_description": "Надсилайте користувачам підтверджувальний лист електронною поштою, коли вони реєструються або змінюють свою адресу електронної пошти.", + "email_verification": "Підтвердження електронної пошти", + "email_verification_description": "Надсилати лист підтвердження користувачам під час реєстрації або зміни електронної адреси.", "email_verification_success_title": "Електронна адреса успішно підтверджена", "email_verification_success_description": "Ваша електронна адреса була успішно підтверджена.", "email_verification_error_title": "Перевірка електронної адреси не вдалася", - "mark_as_unverified": "Позначити як неперевірене", - "mark_as_verified": "Позначити як перевірене", - "email_verification_sent": "Електронний лист для підтвердження надіслано успішно.", - "emails_verified_by_default": "Електронні листи перевіряються за замовчуванням", - "emails_verified_by_default_description": "Якщо ця опція увімкнена, адреси електронної пошти користувачів будуть позначатися як підтверджені за замовчуванням під час реєстрації або при зміні адреси електронної пошти." + "mark_as_unverified": "Позначити як непідтверджену", + "mark_as_verified": "Позначити як підтверджену", + "email_verification_sent": "Електронний лист для підтвердження успішно надіслано.", + "emails_verified_by_default": "Електронні адреси підтверджені за замовчуванням", + "emails_verified_by_default_description": "Якщо увімкнено, електронні адреси користувачів будуть автоматично позначатися як підтверджені під час реєстрації або зміни електронної адреси." } diff --git a/frontend/messages/vi.json b/frontend/messages/vi.json index 611a887c..8d861e5a 100644 --- a/frontend/messages/vi.json +++ b/frontend/messages/vi.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "Nhập mã đã hiển thị ở bước trước.", "authorize": "Cho phép", "federated_client_credentials": "Thông Tin Xác Thực Của Federated Clients", - "federated_client_credentials_description": "Sử dụng thông tin xác thực của federated client, bạn có thể xác thực các client OIDC bằng cách sử dụng token JWT được cấp bởi các bên thứ ba.", "add_federated_client_credential": "Thêm thông tin xác thực cho federated clients", "add_another_federated_client_credential": "Thêm một thông tin xác thực cho federated clients khác", "oidc_allowed_group_count": "Số lượng nhóm được phép", diff --git a/frontend/messages/zh-CN.json b/frontend/messages/zh-CN.json index 5b051731..99b9a987 100644 --- a/frontend/messages/zh-CN.json +++ b/frontend/messages/zh-CN.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "输入在上一步中显示的代码", "authorize": "授权", "federated_client_credentials": "联合身份", - "federated_client_credentials_description": "您可以使用联合身份,通过第三方授权机构签发的 JWT 令牌,对 OIDC 客户端进行认证。", "add_federated_client_credential": "添加联合身份", "add_another_federated_client_credential": "再添加一个联合身份", "oidc_allowed_group_count": "允许的群组数量", diff --git a/frontend/messages/zh-TW.json b/frontend/messages/zh-TW.json index 58c57e5a..eac55e18 100644 --- a/frontend/messages/zh-TW.json +++ b/frontend/messages/zh-TW.json @@ -365,7 +365,6 @@ "enter_code_displayed_in_previous_step": "請輸入上一步顯示的代碼。", "authorize": "授權", "federated_client_credentials": "聯邦身分", - "federated_client_credentials_description": "使用聯邦身分,您可以透過由第三方授權機構簽發的 JWT 令牌來驗證 OIDC 客戶端。", "add_federated_client_credential": "增加聯邦身分", "add_another_federated_client_credential": "新增另一組聯邦身分", "oidc_allowed_group_count": "允許的群組數量", diff --git a/frontend/package.json b/frontend/package.json index 1fa96a18..4699106c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "pocket-id-frontend", - "version": "2.2.0", + "version": "2.3.0", "private": true, "type": "module", "scripts": { @@ -33,7 +33,7 @@ "@internationalized/date": "^3.11.0", "@lucide/svelte": "^0.559.0", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.53.0", + "@sveltejs/kit": "^2.53.4", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/eslint": "^9.6.1", "@types/node": "^24.10.13", @@ -49,7 +49,7 @@ "prettier-plugin-svelte": "^3.5.0", "prettier-plugin-tailwindcss": "^0.7.2", "rollup": "^4.59.0", - "svelte": "^5.53.2", + "svelte": "^5.53.6", "svelte-check": "^4.4.3", "svelte-sonner": "^1.0.7", "tailwind-variants": "^3.2.2", @@ -58,6 +58,7 @@ "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", "typescript-eslint": "^8.56.0", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vite-plugin-compression": "^0.5.1" } } diff --git a/frontend/project.inlang/settings.json b/frontend/project.inlang/settings.json index cdaa7297..7e6525ac 100644 --- a/frontend/project.inlang/settings.json +++ b/frontend/project.inlang/settings.json @@ -13,9 +13,11 @@ "it", "ja", "ko", + "lv", "nl", "no", "pl", + "pt", "pt-BR", "ru", "sv", diff --git a/frontend/src/lib/components/signup/signup-form.svelte b/frontend/src/lib/components/signup/signup-form.svelte index f0a0ca41..ea57f85c 100644 --- a/frontend/src/lib/components/signup/signup-form.svelte +++ b/frontend/src/lib/components/signup/signup-form.svelte @@ -26,7 +26,7 @@ }; const formSchema = z.object({ - firstName: z.string().min(1).max(50), + firstName: z.string().max(50), lastName: emptyToUndefined(z.string().max(50).optional()), username: usernameSchema, email: get(appConfigStore).requireUserEmail ? z.email() : emptyToUndefined(z.email().optional()) @@ -52,12 +52,12 @@
diff --git a/frontend/src/lib/utils/form-util.ts b/frontend/src/lib/utils/form-util.ts index 3c5f0313..7c665608 100644 --- a/frontend/src/lib/utils/form-util.ts +++ b/frontend/src/lib/utils/form-util.ts @@ -122,6 +122,11 @@ export function createForm