From d94530a716358895b01b65babd77226fab69f494 Mon Sep 17 00:00:00 2001 From: borisroman Date: Tue, 20 Feb 2024 11:33:37 +0100 Subject: [PATCH 001/262] feat: add `include_credential` query param to `/admin/identities` list call (#3343) --- identity/handler.go | 32 +++++++++++++++++++++++++++-- identity/handler_test.go | 23 ++++++++++++++++++++- identity/pool.go | 1 + internal/client-go/api_identity.go | 16 +++++++++++++++ internal/httpclient/api_identity.go | 16 +++++++++++++++ spec/api.json | 11 ++++++++++ spec/swagger.json | 9 ++++++++ 7 files changed, 105 insertions(+), 3 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index c821a13de844..0343567a0ac7 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -162,6 +162,15 @@ type listIdentitiesParameters struct { // in: query CredentialsIdentifierSimilar string `json:"preview_credentials_identifier_similar"` + // Include Credentials in Response + // + // Include any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return + // the initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available. + // + // required: false + // in: query + DeclassifyCredentials []string `json:"include_credential"` + crdbx.ConsistencyRequestParameters } @@ -183,6 +192,18 @@ type listIdentitiesParameters struct { // 200: listIdentities // default: errorGeneric func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + includeCredentials := r.URL.Query()["include_credential"] + var declassify []CredentialsType + for _, v := range includeCredentials { + tc, ok := ParseCredentialsType(v) + if ok { + declassify = append(declassify, tc) + } else { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid value `%s` for parameter `include_credential`.", declassify))) + return + } + } + var ( err error params = ListIdentityParameters{ @@ -191,13 +212,14 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para CredentialsIdentifier: r.URL.Query().Get("credentials_identifier"), CredentialsIdentifierSimilar: r.URL.Query().Get("preview_credentials_identifier_similar"), ConsistencyLevel: crdbx.ConsistencyLevelFromRequest(r), + DeclassifyCredentials: declassify, } ) if params.CredentialsIdentifier != "" && params.CredentialsIdentifierSimilar != "" { h.r.Writer().WriteError(w, r, herodot.ErrBadRequest.WithReason("Cannot pass both credentials_identifier and preview_credentials_identifier_similar.")) return } - if params.CredentialsIdentifier != "" || params.CredentialsIdentifierSimilar != "" { + if params.CredentialsIdentifier != "" || params.CredentialsIdentifierSimilar != "" || len(params.DeclassifyCredentials) > 0 { params.Expand = ExpandEverything } params.KeySetPagination, params.PagePagination, err = x.ParseKeysetOrPagePagination(r) @@ -231,7 +253,13 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para // Identities using the marshaler for including metadata_admin isam := make([]WithCredentialsMetadataAndAdminMetadataInJSON, len(is)) for i, identity := range is { - isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(identity) + emit, err := identity.WithDeclassifiedCredentials(r.Context(), h.r, params.DeclassifyCredentials) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(*emit) } h.r.Writer().Write(w, r, isam) diff --git a/identity/handler_test.go b/identity/handler_test.go index ab20e8780ca6..c28d67638266 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -1302,7 +1302,7 @@ func TestHandler(t *testing.T) { }) t.Run("case=should list all identities", func(t *testing.T) { - for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { res := get(t, ts, "/identities", http.StatusOK) assert.False(t, res.Get("0.credentials").Exists(), "credentials config should be omitted: %s", res.Raw) @@ -1313,6 +1313,27 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=should list all identities with credentials", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + res := get(t, ts, "/identities?include_credential=totp", http.StatusOK) + assert.True(t, res.Get("0.credentials").Exists(), "credentials config should be included: %s", res.Raw) + assert.True(t, res.Get("0.metadata_public").Exists(), "metadata_public config should be included: %s", res.Raw) + assert.True(t, res.Get("0.metadata_admin").Exists(), "metadata_admin config should be included: %s", res.Raw) + assert.EqualValues(t, "baz", res.Get(`#(traits.bar=="baz").traits.bar`).String(), "%s", res.Raw) + }) + } + }) + + t.Run("case=should not be able to list all identities with credentials due to wrong credentials type", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + res := get(t, ts, "/identities?include_credential=XYZ", http.StatusBadRequest) + assert.Contains(t, res.Get("error.message").String(), "The request was malformed or contained invalid parameters", "%s", res.Raw) + }) + } + }) + t.Run("case=should list all identities with eventual consistency", func(t *testing.T) { for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { diff --git a/identity/pool.go b/identity/pool.go index 5316f8a53ff9..89eaf9927637 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -21,6 +21,7 @@ type ( IdsFilter []string CredentialsIdentifier string CredentialsIdentifierSimilar string + DeclassifyCredentials []CredentialsType KeySetPagination []keysetpagination.Option // DEPRECATED PagePagination *x.Page diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index bc1b675876fb..c3c361d16ad4 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -2063,6 +2063,7 @@ type IdentityApiApiListIdentitiesRequest struct { ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -2097,6 +2098,10 @@ func (r IdentityApiApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar r.previewCredentialsIdentifierSimilar = &previewCredentialsIdentifierSimilar return r } +func (r IdentityApiApiListIdentitiesRequest) IncludeCredential(includeCredential []string) IdentityApiApiListIdentitiesRequest { + r.includeCredential = &includeCredential + return r +} func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -2172,6 +2177,17 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.previewCredentialsIdentifierSimilar != nil { localVarQueryParams.Add("preview_credentials_identifier_similar", parameterToString(*r.previewCredentialsIdentifierSimilar, "")) } + if r.includeCredential != nil { + t := *r.includeCredential + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("include_credential", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) + } + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index bc1b675876fb..c3c361d16ad4 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -2063,6 +2063,7 @@ type IdentityApiApiListIdentitiesRequest struct { ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -2097,6 +2098,10 @@ func (r IdentityApiApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar r.previewCredentialsIdentifierSimilar = &previewCredentialsIdentifierSimilar return r } +func (r IdentityApiApiListIdentitiesRequest) IncludeCredential(includeCredential []string) IdentityApiApiListIdentitiesRequest { + r.includeCredential = &includeCredential + return r +} func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -2172,6 +2177,17 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.previewCredentialsIdentifierSimilar != nil { localVarQueryParams.Add("preview_credentials_identifier_similar", parameterToString(*r.previewCredentialsIdentifierSimilar, "")) } + if r.includeCredential != nil { + t := *r.includeCredential + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("include_credential", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) + } + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/spec/api.json b/spec/api.json index c35d62d194e7..428393cf6eb8 100644 --- a/spec/api.json +++ b/spec/api.json @@ -3628,6 +3628,17 @@ "schema": { "type": "string" } + }, + { + "description": "Include Credentials in Response\n\nInclude any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return\nthe initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available.", + "in": "query", + "name": "include_credential", + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index 9f074141f069..a3e4d454bad1 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -252,6 +252,15 @@ "description": "This is an EXPERIMENTAL parameter that WILL CHANGE. Do NOT rely on consistent, deterministic behavior.\nTHIS PARAMETER WILL BE REMOVED IN AN UPCOMING RELEASE WITHOUT ANY MIGRATION PATH.\n\nCredentialsIdentifierSimilar is the (partial) identifier (username, email) of the credentials to look up using similarity search.\nOnly one of CredentialsIdentifier and CredentialsIdentifierSimilar can be used.", "name": "preview_credentials_identifier_similar", "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Include Credentials in Response\n\nInclude any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return\nthe initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available.", + "name": "include_credential", + "in": "query" } ], "responses": { From d755fbb2d33f350fd8c5b638e40e5186d72f82c9 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:26:34 +0000 Subject: [PATCH 002/262] autogen(docs): generate and bump docs [skip ci] --- quickstart.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quickstart.yml b/quickstart.yml index 1af5775bdd4c..d674f51a4458 100644 --- a/quickstart.yml +++ b/quickstart.yml @@ -1,7 +1,7 @@ version: '3.7' services: kratos-migrate: - image: oryd/kratos:v1.0.0 + image: oryd/kratos:v1.1.0 environment: - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc volumes: @@ -17,7 +17,7 @@ services: networks: - intranet kratos-selfservice-ui-node: - image: oryd/kratos-selfservice-ui-node:v1.0.0 + image: oryd/kratos-selfservice-ui-node:v1.1.0 environment: - KRATOS_PUBLIC_URL=http://kratos:4433/ - KRATOS_BROWSER_URL=http://127.0.0.1:4433/ @@ -27,7 +27,7 @@ services: kratos: depends_on: - kratos-migrate - image: oryd/kratos:v1.0.0 + image: oryd/kratos:v1.1.0 ports: - '4433:4433' # public - '4434:4434' # admin From 6638c3e812f71901c0f91f8643471db81020f411 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:26:55 +0000 Subject: [PATCH 003/262] autogen: add v1.1.0 to version.schema.json [skip ci] --- .schema/version.schema.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.schema/version.schema.json b/.schema/version.schema.json index e04013ff2587..1676b52a06a5 100644 --- a/.schema/version.schema.json +++ b/.schema/version.schema.json @@ -2,6 +2,23 @@ "$id": "https://github.com/ory/kratos/.schema/versions.config.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "oneOf": [ + { + "allOf": [ + { + "properties": { + "version": { + "const": "v1.1.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v1.1.0/.schemastore/config.schema.json" + } + ] + }, { "allOf": [ { From b291c959c18c72f5edc55607ab23b4592faf8d53 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 20 Feb 2024 16:15:42 +0100 Subject: [PATCH 004/262] fix: add sms mfa via parameter to spec (#3766) --- internal/client-go/api_frontend.go | 8 ++++++++ internal/httpclient/api_frontend.go | 8 ++++++++ selfservice/flow/login/handler.go | 5 +++++ spec/api.json | 8 ++++++++ spec/swagger.json | 6 ++++++ 5 files changed, 35 insertions(+) diff --git a/internal/client-go/api_frontend.go b/internal/client-go/api_frontend.go index 94c0d05a6dba..4e27c89f1f12 100644 --- a/internal/client-go/api_frontend.go +++ b/internal/client-go/api_frontend.go @@ -942,6 +942,7 @@ type FrontendApiApiCreateBrowserLoginFlowRequest struct { cookie *string loginChallenge *string organization *string + via *string } func (r FrontendApiApiCreateBrowserLoginFlowRequest) Refresh(refresh bool) FrontendApiApiCreateBrowserLoginFlowRequest { @@ -968,6 +969,10 @@ func (r FrontendApiApiCreateBrowserLoginFlowRequest) Organization(organization s r.organization = &organization return r } +func (r FrontendApiApiCreateBrowserLoginFlowRequest) Via(via string) FrontendApiApiCreateBrowserLoginFlowRequest { + r.via = &via + return r +} func (r FrontendApiApiCreateBrowserLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.CreateBrowserLoginFlowExecute(r) @@ -1049,6 +1054,9 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat if r.organization != nil { localVarQueryParams.Add("organization", parameterToString(*r.organization, "")) } + if r.via != nil { + localVarQueryParams.Add("via", parameterToString(*r.via, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/api_frontend.go b/internal/httpclient/api_frontend.go index 94c0d05a6dba..4e27c89f1f12 100644 --- a/internal/httpclient/api_frontend.go +++ b/internal/httpclient/api_frontend.go @@ -942,6 +942,7 @@ type FrontendApiApiCreateBrowserLoginFlowRequest struct { cookie *string loginChallenge *string organization *string + via *string } func (r FrontendApiApiCreateBrowserLoginFlowRequest) Refresh(refresh bool) FrontendApiApiCreateBrowserLoginFlowRequest { @@ -968,6 +969,10 @@ func (r FrontendApiApiCreateBrowserLoginFlowRequest) Organization(organization s r.organization = &organization return r } +func (r FrontendApiApiCreateBrowserLoginFlowRequest) Via(via string) FrontendApiApiCreateBrowserLoginFlowRequest { + r.via = &via + return r +} func (r FrontendApiApiCreateBrowserLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.CreateBrowserLoginFlowExecute(r) @@ -1049,6 +1054,9 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat if r.organization != nil { localVarQueryParams.Add("organization", parameterToString(*r.organization, "")) } + if r.via != nil { + localVarQueryParams.Add("via", parameterToString(*r.via, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index ab7fe85245e2..b90218ef4fdd 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -400,6 +400,11 @@ type createBrowserLoginFlow struct { // required: false // in: query Organization string `json:"organization"` + + // Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows. + // + // in: query + Via string `json:"via"` } // swagger:route GET /self-service/login/browser frontend createBrowserLoginFlow diff --git a/spec/api.json b/spec/api.json index 428393cf6eb8..17c3e89f193d 100644 --- a/spec/api.json +++ b/spec/api.json @@ -5319,6 +5319,14 @@ "schema": { "type": "string" } + }, + { + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "in": "query", + "name": "via", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index a3e4d454bad1..4d97d50bb9b7 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1669,6 +1669,12 @@ "description": "An optional organization ID that should be used for logging this user in.\nThis parameter is only effective in the Ory Network.", "name": "organization", "in": "query" + }, + { + "type": "string", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "name": "via", + "in": "query" } ], "responses": { From b8b747b2adc59c8cf938a0ee30accdb4135634b8 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Wed, 21 Feb 2024 10:04:53 +0100 Subject: [PATCH 005/262] feat: add transient payloads to all flows (#3738) --- courier/template/email/login_code_valid.go | 9 ++- .../template/email/recovery_code_invalid.go | 5 +- courier/template/email/recovery_code_valid.go | 9 ++- courier/template/email/recovery_invalid.go | 5 +- courier/template/email/recovery_valid.go | 9 ++- .../template/email/registration_code_valid.go | 1 + .../email/verification_code_invalid.go | 5 +- .../template/email/verification_code_valid.go | 1 + .../template/email/verification_invalid.go | 5 +- courier/template/email/verification_valid.go | 9 ++- courier/template/sms/login_code_valid.go | 9 ++- courier/template/sms/verification_code.go | 1 + internal/client-go/model_login_flow.go | 37 +++++++++ internal/client-go/model_recovery_flow.go | 37 +++++++++ internal/client-go/model_settings_flow.go | 37 +++++++++ ...odel_update_login_flow_with_code_method.go | 37 +++++++++ ...odel_update_login_flow_with_oidc_method.go | 37 +++++++++ ..._update_login_flow_with_password_method.go | 37 +++++++++ ...odel_update_login_flow_with_totp_method.go | 37 +++++++++ ...update_login_flow_with_web_authn_method.go | 37 +++++++++ ...l_update_recovery_flow_with_code_method.go | 37 +++++++++ ...l_update_recovery_flow_with_link_method.go | 37 +++++++++ ...update_settings_flow_with_lookup_method.go | 37 +++++++++ ...l_update_settings_flow_with_oidc_method.go | 37 +++++++++ ...date_settings_flow_with_password_method.go | 37 +++++++++ ...pdate_settings_flow_with_profile_method.go | 37 +++++++++ ...l_update_settings_flow_with_totp_method.go | 37 +++++++++ ...ate_settings_flow_with_web_authn_method.go | 37 +++++++++ ...date_verification_flow_with_code_method.go | 37 +++++++++ ...date_verification_flow_with_link_method.go | 37 +++++++++ internal/client-go/model_verification_flow.go | 37 +++++++++ internal/httpclient/model_login_flow.go | 37 +++++++++ internal/httpclient/model_recovery_flow.go | 37 +++++++++ internal/httpclient/model_settings_flow.go | 37 +++++++++ ...odel_update_login_flow_with_code_method.go | 37 +++++++++ ...odel_update_login_flow_with_oidc_method.go | 37 +++++++++ ..._update_login_flow_with_password_method.go | 37 +++++++++ ...odel_update_login_flow_with_totp_method.go | 37 +++++++++ ...update_login_flow_with_web_authn_method.go | 37 +++++++++ ...l_update_recovery_flow_with_code_method.go | 37 +++++++++ ...l_update_recovery_flow_with_link_method.go | 37 +++++++++ ...update_settings_flow_with_lookup_method.go | 37 +++++++++ ...l_update_settings_flow_with_oidc_method.go | 37 +++++++++ ...date_settings_flow_with_password_method.go | 37 +++++++++ ...pdate_settings_flow_with_profile_method.go | 37 +++++++++ ...l_update_settings_flow_with_totp_method.go | 37 +++++++++ ...ate_settings_flow_with_web_authn_method.go | 37 +++++++++ ...date_verification_flow_with_code_method.go | 37 +++++++++ ...date_verification_flow_with_link_method.go | 37 +++++++++ .../httpclient/model_verification_flow.go | 37 +++++++++ selfservice/flow/error_test.go | 4 + selfservice/flow/flow.go | 2 + selfservice/flow/login/flow.go | 9 +++ selfservice/flow/recovery/flow.go | 9 +++ selfservice/flow/recovery/handler.go | 1 + selfservice/flow/registration/flow.go | 6 ++ selfservice/flow/settings/flow.go | 9 +++ selfservice/flow/verification/flow.go | 9 +++ selfservice/hook/web_hook_integration_test.go | 63 +++++---------- .../strategy/code/.schema/login.schema.json | 4 + .../code/.schema/recovery.schema.json | 4 + .../code/.schema/verification.schema.json | 4 + selfservice/strategy/code/code_sender.go | 72 ++++++++++++----- selfservice/strategy/code/strategy_login.go | 8 ++ .../strategy/code/strategy_recovery.go | 19 +++-- .../strategy/code/strategy_registration.go | 12 +-- .../strategy/code/strategy_verification.go | 8 ++ .../link/.schema/recovery.schema.json | 4 + .../link/.schema/verification.schema.json | 4 + selfservice/strategy/link/sender.go | 73 ++++++++++++----- .../strategy/link/strategy_recovery.go | 19 +++-- .../strategy/link/strategy_verification.go | 18 +++-- .../strategy/lookup/.schema/login.schema.json | 4 + .../lookup/.schema/settings.schema.json | 4 + selfservice/strategy/lookup/settings.go | 5 ++ .../oidc/.schema/settings.schema.json | 4 + selfservice/strategy/oidc/strategy_login.go | 7 ++ .../strategy/oidc/strategy_registration.go | 19 ++--- .../strategy/oidc/strategy_settings.go | 32 +++++--- .../password/.schema/login.schema.json | 4 + .../password/.schema/settings.schema.json | 4 + selfservice/strategy/password/login.go | 1 + selfservice/strategy/password/registration.go | 2 +- selfservice/strategy/password/settings.go | 5 ++ selfservice/strategy/password/types.go | 12 ++- .../profile/.schema/settings.schema.json | 4 + selfservice/strategy/profile/strategy.go | 5 ++ .../strategy/totp/.schema/login.schema.json | 4 + .../totp/.schema/settings.schema.json | 4 + selfservice/strategy/totp/login.go | 6 ++ selfservice/strategy/totp/settings.go | 5 ++ .../webauthn/.schema/login.schema.json | 4 + .../webauthn/.schema/settings.schema.json | 4 + selfservice/strategy/webauthn/login.go | 6 ++ selfservice/strategy/webauthn/registration.go | 2 +- selfservice/strategy/webauthn/settings.go | 5 ++ spec/api.json | 76 ++++++++++++++++++ spec/swagger.json | 76 ++++++++++++++++++ test/e2e/cypress/downloads/downloads.html | Bin 0 -> 4680 bytes test/e2e/cypress/helpers/webhook.ts | 47 ++++++----- .../mobile/registration/success.spec.ts | 10 ++- .../oidc/registration/success.spec.ts | 10 ++- .../profiles/passwordless/flows.spec.ts | 10 ++- .../profiles/webhoooks/login/success.spec.ts | 12 +++ .../webhoooks/registration/success.spec.ts | 19 ++--- x/json_marshal.go | 18 +++++ x/json_marshal_test.go | 52 ++++++++++++ 107 files changed, 2117 insertions(+), 200 deletions(-) create mode 100644 test/e2e/cypress/downloads/downloads.html create mode 100644 x/json_marshal.go create mode 100644 x/json_marshal_test.go diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go index f48ae6116118..b09f70f8625e 100644 --- a/courier/template/email/login_code_valid.go +++ b/courier/template/email/login_code_valid.go @@ -18,10 +18,11 @@ type ( model *LoginCodeValidModel } LoginCodeValidModel struct { - To string `json:"to"` - LoginCode string `json:"login_code"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + LoginCode string `json:"login_code"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_code_invalid.go b/courier/template/email/recovery_code_invalid.go index e2f648003271..c2852e01b12b 100644 --- a/courier/template/email/recovery_code_invalid.go +++ b/courier/template/email/recovery_code_invalid.go @@ -18,8 +18,9 @@ type ( model *RecoveryCodeInvalidModel } RecoveryCodeInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_code_valid.go b/courier/template/email/recovery_code_valid.go index f9e2ad9ec20f..4e8992da3d1d 100644 --- a/courier/template/email/recovery_code_valid.go +++ b/courier/template/email/recovery_code_valid.go @@ -18,10 +18,11 @@ type ( model *RecoveryCodeValidModel } RecoveryCodeValidModel struct { - To string `json:"to"` - RecoveryCode string `json:"recovery_code"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RecoveryCode string `json:"recovery_code"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_invalid.go b/courier/template/email/recovery_invalid.go index 38d70d44bdc9..81bae893de23 100644 --- a/courier/template/email/recovery_invalid.go +++ b/courier/template/email/recovery_invalid.go @@ -18,8 +18,9 @@ type ( m *RecoveryInvalidModel } RecoveryInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_valid.go b/courier/template/email/recovery_valid.go index 18e4fde7bd66..f82a40b4f919 100644 --- a/courier/template/email/recovery_valid.go +++ b/courier/template/email/recovery_valid.go @@ -18,10 +18,11 @@ type ( m *RecoveryValidModel } RecoveryValidModel struct { - To string `json:"to"` - RecoveryURL string `json:"recovery_url"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RecoveryURL string `json:"recovery_url"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go index e63812b00b80..ec52362a8990 100644 --- a/courier/template/email/registration_code_valid.go +++ b/courier/template/email/registration_code_valid.go @@ -22,6 +22,7 @@ type ( Traits map[string]interface{} `json:"traits"` RegistrationCode string `json:"registration_code"` RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_code_invalid.go b/courier/template/email/verification_code_invalid.go index 77aec0c04c3e..a7ebad3be8cb 100644 --- a/courier/template/email/verification_code_invalid.go +++ b/courier/template/email/verification_code_invalid.go @@ -18,8 +18,9 @@ type ( m *VerificationCodeInvalidModel } VerificationCodeInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_code_valid.go b/courier/template/email/verification_code_valid.go index 7cf5823b0524..bd2045a03d25 100644 --- a/courier/template/email/verification_code_valid.go +++ b/courier/template/email/verification_code_valid.go @@ -23,6 +23,7 @@ type ( VerificationCode string `json:"verification_code"` Identity map[string]interface{} `json:"identity"` RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_invalid.go b/courier/template/email/verification_invalid.go index 7b0a776fa254..08485c92e94f 100644 --- a/courier/template/email/verification_invalid.go +++ b/courier/template/email/verification_invalid.go @@ -18,8 +18,9 @@ type ( m *VerificationInvalidModel } VerificationInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_valid.go b/courier/template/email/verification_valid.go index c04913953519..eb9578261d83 100644 --- a/courier/template/email/verification_valid.go +++ b/courier/template/email/verification_valid.go @@ -18,10 +18,11 @@ type ( m *VerificationValidModel } VerificationValidModel struct { - To string `json:"to"` - VerificationURL string `json:"verification_url"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + VerificationURL string `json:"verification_url"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/sms/login_code_valid.go b/courier/template/sms/login_code_valid.go index 439856accb8a..f365a6ba2800 100644 --- a/courier/template/sms/login_code_valid.go +++ b/courier/template/sms/login_code_valid.go @@ -17,10 +17,11 @@ type ( model *LoginCodeValidModel } LoginCodeValidModel struct { - To string `json:"to"` - LoginCode string `json:"login_code"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + LoginCode string `json:"login_code"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/sms/verification_code.go b/courier/template/sms/verification_code.go index a367de9e584c..4204df0ac4c8 100644 --- a/courier/template/sms/verification_code.go +++ b/courier/template/sms/verification_code.go @@ -22,6 +22,7 @@ type ( VerificationCode string `json:"verification_code"` Identity map[string]interface{} `json:"identity"` RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/internal/client-go/model_login_flow.go b/internal/client-go/model_login_flow.go index b36abc379568..777e9b8a7316 100644 --- a/internal/client-go/model_login_flow.go +++ b/internal/client-go/model_login_flow.go @@ -43,6 +43,8 @@ type LoginFlow struct { SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the login to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -495,6 +497,38 @@ func (o *LoginFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *LoginFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *LoginFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *LoginFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -619,6 +653,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_recovery_flow.go b/internal/client-go/model_recovery_flow.go index e6df63a7c6b2..56f27a904be1 100644 --- a/internal/client-go/model_recovery_flow.go +++ b/internal/client-go/model_recovery_flow.go @@ -34,6 +34,8 @@ type RecoveryFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the recovery flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -281,6 +283,38 @@ func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *RecoveryFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RecoveryFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *RecoveryFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *RecoveryFlow) GetType() string { if o == nil { @@ -355,6 +389,9 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_settings_flow.go b/internal/client-go/model_settings_flow.go index fa5cd9317c54..f45c1599e8dd 100644 --- a/internal/client-go/model_settings_flow.go +++ b/internal/client-go/model_settings_flow.go @@ -35,6 +35,8 @@ type SettingsFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. State interface{} `json:"state"` + // TransientPayload is used to pass data from the settings flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -307,6 +309,38 @@ func (o *SettingsFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *SettingsFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *SettingsFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *SettingsFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *SettingsFlow) GetType() string { if o == nil { @@ -384,6 +418,9 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_update_login_flow_with_code_method.go b/internal/client-go/model_update_login_flow_with_code_method.go index bd97ab583ebc..5833200a3ce9 100644 --- a/internal/client-go/model_update_login_flow_with_code_method.go +++ b/internal/client-go/model_update_login_flow_with_code_method.go @@ -27,6 +27,8 @@ type UpdateLoginFlowWithCodeMethod struct { Method string `json:"method"` // Resend is set when the user wants to resend the code Resend *string `json:"resend,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object @@ -192,6 +194,38 @@ func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { o.Resend = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -209,6 +243,9 @@ func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if o.Resend != nil { toSerialize["resend"] = o.Resend } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_login_flow_with_oidc_method.go b/internal/client-go/model_update_login_flow_with_oidc_method.go index c196eb2121da..8f4a7348b13a 100644 --- a/internal/client-go/model_update_login_flow_with_oidc_method.go +++ b/internal/client-go/model_update_login_flow_with_oidc_method.go @@ -29,6 +29,8 @@ type UpdateLoginFlowWithOidcMethod struct { Provider string `json:"provider"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` } @@ -228,6 +230,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { if o == nil || o.UpstreamParameters == nil { @@ -280,6 +314,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.UpstreamParameters != nil { toSerialize["upstream_parameters"] = o.UpstreamParameters } diff --git a/internal/client-go/model_update_login_flow_with_password_method.go b/internal/client-go/model_update_login_flow_with_password_method.go index 35a9b590bd04..4bad1a416326 100644 --- a/internal/client-go/model_update_login_flow_with_password_method.go +++ b/internal/client-go/model_update_login_flow_with_password_method.go @@ -27,6 +27,8 @@ type UpdateLoginFlowWithPasswordMethod struct { Password string `json:"password"` // Identifier is the email or username of the user trying to log in. This field is deprecated! PasswordIdentifier *string `json:"password_identifier,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithPasswordMethod instantiates a new UpdateLoginFlowWithPasswordMethod object @@ -185,6 +187,38 @@ func (o *UpdateLoginFlowWithPasswordMethod) SetPasswordIdentifier(v string) { o.PasswordIdentifier = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -202,6 +236,9 @@ func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if o.PasswordIdentifier != nil { toSerialize["password_identifier"] = o.PasswordIdentifier } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_login_flow_with_totp_method.go b/internal/client-go/model_update_login_flow_with_totp_method.go index 34d3b316dd01..32a94efb20f4 100644 --- a/internal/client-go/model_update_login_flow_with_totp_method.go +++ b/internal/client-go/model_update_login_flow_with_totp_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithTotpMethod struct { Method string `json:"method"` // The TOTP code. TotpCode string `json:"totp_code"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithTotpMethod instantiates a new UpdateLoginFlowWithTotpMethod object @@ -124,6 +126,38 @@ func (o *UpdateLoginFlowWithTotpMethod) SetTotpCode(v string) { o.TotpCode = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["totp_code"] = o.TotpCode } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_login_flow_with_web_authn_method.go b/internal/client-go/model_update_login_flow_with_web_authn_method.go index d5d8554febb4..1c3211a510ed 100644 --- a/internal/client-go/model_update_login_flow_with_web_authn_method.go +++ b/internal/client-go/model_update_login_flow_with_web_authn_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithWebAuthnMethod struct { Identifier string `json:"identifier"` // Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. WebauthnLogin *string `json:"webauthn_login,omitempty"` } @@ -126,6 +128,38 @@ func (o *UpdateLoginFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnLogin returns the WebauthnLogin field value if set, zero value otherwise. func (o *UpdateLoginFlowWithWebAuthnMethod) GetWebauthnLogin() string { if o == nil || o.WebauthnLogin == nil { @@ -169,6 +203,9 @@ func (o UpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnLogin != nil { toSerialize["webauthn_login"] = o.WebauthnLogin } diff --git a/internal/client-go/model_update_recovery_flow_with_code_method.go b/internal/client-go/model_update_recovery_flow_with_code_method.go index 8d440330c7b8..50aad2ca2945 100644 --- a/internal/client-go/model_update_recovery_flow_with_code_method.go +++ b/internal/client-go/model_update_recovery_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateRecoveryFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code`. link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithCodeMethod instantiates a new UpdateRecoveryFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateRecoveryFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_recovery_flow_with_link_method.go b/internal/client-go/model_update_recovery_flow_with_link_method.go index 8a8861d86f07..429410cf3c01 100644 --- a/internal/client-go/model_update_recovery_flow_with_link_method.go +++ b/internal/client-go/model_update_recovery_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateRecoveryFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code` link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithLinkMethod instantiates a new UpdateRecoveryFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateRecoveryFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_lookup_method.go b/internal/client-go/model_update_settings_flow_with_lookup_method.go index 96a12cb7f9e2..ca2e89827126 100644 --- a/internal/client-go/model_update_settings_flow_with_lookup_method.go +++ b/internal/client-go/model_update_settings_flow_with_lookup_method.go @@ -29,6 +29,8 @@ type UpdateSettingsFlowWithLookupMethod struct { LookupSecretReveal *bool `json:"lookup_secret_reveal,omitempty"` // Method Should be set to \"lookup\" when trying to add, update, or remove a lookup pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithLookupMethod instantiates a new UpdateSettingsFlowWithLookupMethod object @@ -233,6 +235,38 @@ func (o *UpdateSettingsFlowWithLookupMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithLookupMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -253,6 +287,9 @@ func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_oidc_method.go b/internal/client-go/model_update_settings_flow_with_oidc_method.go index 3008436d8b27..c54a0d1251f3 100644 --- a/internal/client-go/model_update_settings_flow_with_oidc_method.go +++ b/internal/client-go/model_update_settings_flow_with_oidc_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithOidcMethod struct { Method string `json:"method"` // The identity's traits in: body Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Unlink this provider Either this or `link` must be set. type: string in: body Unlink *string `json:"unlink,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -169,6 +171,38 @@ func (o *UpdateSettingsFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUnlink returns the Unlink field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithOidcMethod) GetUnlink() string { if o == nil || o.Unlink == nil { @@ -247,6 +281,9 @@ func (o UpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.Unlink != nil { toSerialize["unlink"] = o.Unlink } diff --git a/internal/client-go/model_update_settings_flow_with_password_method.go b/internal/client-go/model_update_settings_flow_with_password_method.go index d4b9934756f0..450cfdc4fb2b 100644 --- a/internal/client-go/model_update_settings_flow_with_password_method.go +++ b/internal/client-go/model_update_settings_flow_with_password_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithPasswordMethod struct { Method string `json:"method"` // Password is the updated password Password string `json:"password"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithPasswordMethod instantiates a new UpdateSettingsFlowWithPasswordMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithPasswordMethod) SetPassword(v string) { o.Password = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["password"] = o.Password } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_profile_method.go b/internal/client-go/model_update_settings_flow_with_profile_method.go index af2051f6c38c..f208e2b5fb06 100644 --- a/internal/client-go/model_update_settings_flow_with_profile_method.go +++ b/internal/client-go/model_update_settings_flow_with_profile_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithProfileMethod struct { Method string `json:"method"` // Traits The identity's traits. Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithProfileMethod instantiates a new UpdateSettingsFlowWithProfileMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithProfileMethod) SetTraits(v map[string]interface{} o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_totp_method.go b/internal/client-go/model_update_settings_flow_with_totp_method.go index 565f5d0e8a83..d36d5a00ab53 100644 --- a/internal/client-go/model_update_settings_flow_with_totp_method.go +++ b/internal/client-go/model_update_settings_flow_with_totp_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithTotpMethod struct { TotpCode *string `json:"totp_code,omitempty"` // UnlinkTOTP if true will remove the TOTP pairing, effectively removing the credential. This can be used to set up a new TOTP device. TotpUnlink *bool `json:"totp_unlink,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithTotpMethod instantiates a new UpdateSettingsFlowWithTotpMethod object @@ -165,6 +167,38 @@ func (o *UpdateSettingsFlowWithTotpMethod) SetTotpUnlink(v bool) { o.TotpUnlink = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -179,6 +213,9 @@ func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if o.TotpUnlink != nil { toSerialize["totp_unlink"] = o.TotpUnlink } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_web_authn_method.go b/internal/client-go/model_update_settings_flow_with_web_authn_method.go index 7bcd90c82e58..d09d0def049c 100644 --- a/internal/client-go/model_update_settings_flow_with_web_authn_method.go +++ b/internal/client-go/model_update_settings_flow_with_web_authn_method.go @@ -21,6 +21,8 @@ type UpdateSettingsFlowWithWebAuthnMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // Method Should be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. WebauthnRegister *string `json:"webauthn_register,omitempty"` // Name of the WebAuthn Security Key to be Added A human-readable name for the security key which will be added. @@ -103,6 +105,38 @@ func (o *UpdateSettingsFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnRegister returns the WebauthnRegister field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegister() string { if o == nil || o.WebauthnRegister == nil { @@ -207,6 +241,9 @@ func (o UpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnRegister != nil { toSerialize["webauthn_register"] = o.WebauthnRegister } diff --git a/internal/client-go/model_update_verification_flow_with_code_method.go b/internal/client-go/model_update_verification_flow_with_code_method.go index 4548425684b9..e6821735a296 100644 --- a/internal/client-go/model_update_verification_flow_with_code_method.go +++ b/internal/client-go/model_update_verification_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateVerificationFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code`. link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithCodeMethod instantiates a new UpdateVerificationFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateVerificationFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_verification_flow_with_link_method.go b/internal/client-go/model_update_verification_flow_with_link_method.go index 8ebbbd749952..b7ab49d3d086 100644 --- a/internal/client-go/model_update_verification_flow_with_link_method.go +++ b/internal/client-go/model_update_verification_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateVerificationFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code` link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithLinkMethod instantiates a new UpdateVerificationFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateVerificationFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_verification_flow.go b/internal/client-go/model_verification_flow.go index c10870c9f841..ae3039ddee24 100644 --- a/internal/client-go/model_verification_flow.go +++ b/internal/client-go/model_verification_flow.go @@ -32,6 +32,8 @@ type VerificationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the verification flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -268,6 +270,38 @@ func (o *VerificationFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *VerificationFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *VerificationFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *VerificationFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *VerificationFlow) GetType() string { if o == nil { @@ -339,6 +373,9 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_login_flow.go b/internal/httpclient/model_login_flow.go index b36abc379568..777e9b8a7316 100644 --- a/internal/httpclient/model_login_flow.go +++ b/internal/httpclient/model_login_flow.go @@ -43,6 +43,8 @@ type LoginFlow struct { SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the login to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -495,6 +497,38 @@ func (o *LoginFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *LoginFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *LoginFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *LoginFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -619,6 +653,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_recovery_flow.go b/internal/httpclient/model_recovery_flow.go index e6df63a7c6b2..56f27a904be1 100644 --- a/internal/httpclient/model_recovery_flow.go +++ b/internal/httpclient/model_recovery_flow.go @@ -34,6 +34,8 @@ type RecoveryFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the recovery flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -281,6 +283,38 @@ func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *RecoveryFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RecoveryFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *RecoveryFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *RecoveryFlow) GetType() string { if o == nil { @@ -355,6 +389,9 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_settings_flow.go b/internal/httpclient/model_settings_flow.go index fa5cd9317c54..f45c1599e8dd 100644 --- a/internal/httpclient/model_settings_flow.go +++ b/internal/httpclient/model_settings_flow.go @@ -35,6 +35,8 @@ type SettingsFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. State interface{} `json:"state"` + // TransientPayload is used to pass data from the settings flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -307,6 +309,38 @@ func (o *SettingsFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *SettingsFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *SettingsFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *SettingsFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *SettingsFlow) GetType() string { if o == nil { @@ -384,6 +418,9 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_update_login_flow_with_code_method.go b/internal/httpclient/model_update_login_flow_with_code_method.go index bd97ab583ebc..5833200a3ce9 100644 --- a/internal/httpclient/model_update_login_flow_with_code_method.go +++ b/internal/httpclient/model_update_login_flow_with_code_method.go @@ -27,6 +27,8 @@ type UpdateLoginFlowWithCodeMethod struct { Method string `json:"method"` // Resend is set when the user wants to resend the code Resend *string `json:"resend,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object @@ -192,6 +194,38 @@ func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { o.Resend = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -209,6 +243,9 @@ func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if o.Resend != nil { toSerialize["resend"] = o.Resend } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_login_flow_with_oidc_method.go b/internal/httpclient/model_update_login_flow_with_oidc_method.go index c196eb2121da..8f4a7348b13a 100644 --- a/internal/httpclient/model_update_login_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_login_flow_with_oidc_method.go @@ -29,6 +29,8 @@ type UpdateLoginFlowWithOidcMethod struct { Provider string `json:"provider"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` } @@ -228,6 +230,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { if o == nil || o.UpstreamParameters == nil { @@ -280,6 +314,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.UpstreamParameters != nil { toSerialize["upstream_parameters"] = o.UpstreamParameters } diff --git a/internal/httpclient/model_update_login_flow_with_password_method.go b/internal/httpclient/model_update_login_flow_with_password_method.go index 35a9b590bd04..4bad1a416326 100644 --- a/internal/httpclient/model_update_login_flow_with_password_method.go +++ b/internal/httpclient/model_update_login_flow_with_password_method.go @@ -27,6 +27,8 @@ type UpdateLoginFlowWithPasswordMethod struct { Password string `json:"password"` // Identifier is the email or username of the user trying to log in. This field is deprecated! PasswordIdentifier *string `json:"password_identifier,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithPasswordMethod instantiates a new UpdateLoginFlowWithPasswordMethod object @@ -185,6 +187,38 @@ func (o *UpdateLoginFlowWithPasswordMethod) SetPasswordIdentifier(v string) { o.PasswordIdentifier = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -202,6 +236,9 @@ func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if o.PasswordIdentifier != nil { toSerialize["password_identifier"] = o.PasswordIdentifier } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_login_flow_with_totp_method.go b/internal/httpclient/model_update_login_flow_with_totp_method.go index 34d3b316dd01..32a94efb20f4 100644 --- a/internal/httpclient/model_update_login_flow_with_totp_method.go +++ b/internal/httpclient/model_update_login_flow_with_totp_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithTotpMethod struct { Method string `json:"method"` // The TOTP code. TotpCode string `json:"totp_code"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithTotpMethod instantiates a new UpdateLoginFlowWithTotpMethod object @@ -124,6 +126,38 @@ func (o *UpdateLoginFlowWithTotpMethod) SetTotpCode(v string) { o.TotpCode = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["totp_code"] = o.TotpCode } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_login_flow_with_web_authn_method.go b/internal/httpclient/model_update_login_flow_with_web_authn_method.go index d5d8554febb4..1c3211a510ed 100644 --- a/internal/httpclient/model_update_login_flow_with_web_authn_method.go +++ b/internal/httpclient/model_update_login_flow_with_web_authn_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithWebAuthnMethod struct { Identifier string `json:"identifier"` // Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. WebauthnLogin *string `json:"webauthn_login,omitempty"` } @@ -126,6 +128,38 @@ func (o *UpdateLoginFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnLogin returns the WebauthnLogin field value if set, zero value otherwise. func (o *UpdateLoginFlowWithWebAuthnMethod) GetWebauthnLogin() string { if o == nil || o.WebauthnLogin == nil { @@ -169,6 +203,9 @@ func (o UpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnLogin != nil { toSerialize["webauthn_login"] = o.WebauthnLogin } diff --git a/internal/httpclient/model_update_recovery_flow_with_code_method.go b/internal/httpclient/model_update_recovery_flow_with_code_method.go index 8d440330c7b8..50aad2ca2945 100644 --- a/internal/httpclient/model_update_recovery_flow_with_code_method.go +++ b/internal/httpclient/model_update_recovery_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateRecoveryFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code`. link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithCodeMethod instantiates a new UpdateRecoveryFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateRecoveryFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_recovery_flow_with_link_method.go b/internal/httpclient/model_update_recovery_flow_with_link_method.go index 8a8861d86f07..429410cf3c01 100644 --- a/internal/httpclient/model_update_recovery_flow_with_link_method.go +++ b/internal/httpclient/model_update_recovery_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateRecoveryFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code` link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithLinkMethod instantiates a new UpdateRecoveryFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateRecoveryFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_lookup_method.go b/internal/httpclient/model_update_settings_flow_with_lookup_method.go index 96a12cb7f9e2..ca2e89827126 100644 --- a/internal/httpclient/model_update_settings_flow_with_lookup_method.go +++ b/internal/httpclient/model_update_settings_flow_with_lookup_method.go @@ -29,6 +29,8 @@ type UpdateSettingsFlowWithLookupMethod struct { LookupSecretReveal *bool `json:"lookup_secret_reveal,omitempty"` // Method Should be set to \"lookup\" when trying to add, update, or remove a lookup pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithLookupMethod instantiates a new UpdateSettingsFlowWithLookupMethod object @@ -233,6 +235,38 @@ func (o *UpdateSettingsFlowWithLookupMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithLookupMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -253,6 +287,9 @@ func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_oidc_method.go b/internal/httpclient/model_update_settings_flow_with_oidc_method.go index 3008436d8b27..c54a0d1251f3 100644 --- a/internal/httpclient/model_update_settings_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_settings_flow_with_oidc_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithOidcMethod struct { Method string `json:"method"` // The identity's traits in: body Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Unlink this provider Either this or `link` must be set. type: string in: body Unlink *string `json:"unlink,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -169,6 +171,38 @@ func (o *UpdateSettingsFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUnlink returns the Unlink field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithOidcMethod) GetUnlink() string { if o == nil || o.Unlink == nil { @@ -247,6 +281,9 @@ func (o UpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.Unlink != nil { toSerialize["unlink"] = o.Unlink } diff --git a/internal/httpclient/model_update_settings_flow_with_password_method.go b/internal/httpclient/model_update_settings_flow_with_password_method.go index d4b9934756f0..450cfdc4fb2b 100644 --- a/internal/httpclient/model_update_settings_flow_with_password_method.go +++ b/internal/httpclient/model_update_settings_flow_with_password_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithPasswordMethod struct { Method string `json:"method"` // Password is the updated password Password string `json:"password"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithPasswordMethod instantiates a new UpdateSettingsFlowWithPasswordMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithPasswordMethod) SetPassword(v string) { o.Password = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["password"] = o.Password } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_profile_method.go b/internal/httpclient/model_update_settings_flow_with_profile_method.go index af2051f6c38c..f208e2b5fb06 100644 --- a/internal/httpclient/model_update_settings_flow_with_profile_method.go +++ b/internal/httpclient/model_update_settings_flow_with_profile_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithProfileMethod struct { Method string `json:"method"` // Traits The identity's traits. Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithProfileMethod instantiates a new UpdateSettingsFlowWithProfileMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithProfileMethod) SetTraits(v map[string]interface{} o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_totp_method.go b/internal/httpclient/model_update_settings_flow_with_totp_method.go index 565f5d0e8a83..d36d5a00ab53 100644 --- a/internal/httpclient/model_update_settings_flow_with_totp_method.go +++ b/internal/httpclient/model_update_settings_flow_with_totp_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithTotpMethod struct { TotpCode *string `json:"totp_code,omitempty"` // UnlinkTOTP if true will remove the TOTP pairing, effectively removing the credential. This can be used to set up a new TOTP device. TotpUnlink *bool `json:"totp_unlink,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithTotpMethod instantiates a new UpdateSettingsFlowWithTotpMethod object @@ -165,6 +167,38 @@ func (o *UpdateSettingsFlowWithTotpMethod) SetTotpUnlink(v bool) { o.TotpUnlink = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -179,6 +213,9 @@ func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if o.TotpUnlink != nil { toSerialize["totp_unlink"] = o.TotpUnlink } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_web_authn_method.go b/internal/httpclient/model_update_settings_flow_with_web_authn_method.go index 7bcd90c82e58..d09d0def049c 100644 --- a/internal/httpclient/model_update_settings_flow_with_web_authn_method.go +++ b/internal/httpclient/model_update_settings_flow_with_web_authn_method.go @@ -21,6 +21,8 @@ type UpdateSettingsFlowWithWebAuthnMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // Method Should be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. WebauthnRegister *string `json:"webauthn_register,omitempty"` // Name of the WebAuthn Security Key to be Added A human-readable name for the security key which will be added. @@ -103,6 +105,38 @@ func (o *UpdateSettingsFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnRegister returns the WebauthnRegister field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegister() string { if o == nil || o.WebauthnRegister == nil { @@ -207,6 +241,9 @@ func (o UpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnRegister != nil { toSerialize["webauthn_register"] = o.WebauthnRegister } diff --git a/internal/httpclient/model_update_verification_flow_with_code_method.go b/internal/httpclient/model_update_verification_flow_with_code_method.go index 4548425684b9..e6821735a296 100644 --- a/internal/httpclient/model_update_verification_flow_with_code_method.go +++ b/internal/httpclient/model_update_verification_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateVerificationFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code`. link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithCodeMethod instantiates a new UpdateVerificationFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateVerificationFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_verification_flow_with_link_method.go b/internal/httpclient/model_update_verification_flow_with_link_method.go index 8ebbbd749952..b7ab49d3d086 100644 --- a/internal/httpclient/model_update_verification_flow_with_link_method.go +++ b/internal/httpclient/model_update_verification_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateVerificationFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code` link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithLinkMethod instantiates a new UpdateVerificationFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateVerificationFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_verification_flow.go b/internal/httpclient/model_verification_flow.go index c10870c9f841..ae3039ddee24 100644 --- a/internal/httpclient/model_verification_flow.go +++ b/internal/httpclient/model_verification_flow.go @@ -32,6 +32,8 @@ type VerificationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the verification flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -268,6 +270,38 @@ func (o *VerificationFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *VerificationFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *VerificationFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *VerificationFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *VerificationFlow) GetType() string { if o == nil { @@ -339,6 +373,9 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/selfservice/flow/error_test.go b/selfservice/flow/error_test.go index 98b1ad32e9c4..3bf62d6091e2 100644 --- a/selfservice/flow/error_test.go +++ b/selfservice/flow/error_test.go @@ -97,6 +97,10 @@ func (t *testFlow) SetState(state State) { t.State = state } +func (t *testFlow) GetTransientPayload() json.RawMessage { + return nil +} + func newTestFlow(r *http.Request, flowType Type) Flow { id := x.NewUUID() requestURL := x.RequestURL(r).String() diff --git a/selfservice/flow/flow.go b/selfservice/flow/flow.go index ee9fbba6638d..d5ad7b740a97 100644 --- a/selfservice/flow/flow.go +++ b/selfservice/flow/flow.go @@ -5,6 +5,7 @@ package flow import ( "context" + "encoding/json" "net/http" "net/url" @@ -39,6 +40,7 @@ type Flow interface { GetState() State SetState(State) GetFlowName() FlowName + GetTransientPayload() json.RawMessage } type FlowWithRedirect interface { diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index 5dd8422cdb84..37044f7ca2d8 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -140,6 +140,11 @@ type Flow struct { // Only used internally RawIDTokenNonce string `json:"-" db:"-"` + + // TransientPayload is used to pass data from the login to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` } var _ flow.Flow = new(Flow) @@ -290,3 +295,7 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state flow.State) { f.State = State(state) } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go index d5aed79ae0f3..9eac423266cb 100644 --- a/selfservice/flow/recovery/flow.go +++ b/selfservice/flow/recovery/flow.go @@ -103,6 +103,11 @@ type Flow struct { // Contains possible actions that could follow this flow ContinueWith []flow.ContinueWith `json:"continue_with,omitempty" faker:"-" db:"-"` + + // TransientPayload is used to pass data from the recovery flow to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` } var _ flow.Flow = new(Flow) @@ -239,3 +244,7 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go index 6927a733dd8e..d5ba1a44dc47 100644 --- a/selfservice/flow/recovery/handler.go +++ b/selfservice/flow/recovery/handler.go @@ -458,6 +458,7 @@ func (h *Handler) updateRecoveryFlow(w http.ResponseWriter, r *http.Request, ps h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, g, err) return } + updatedFlow.TransientPayload = f.TransientPayload h.d.Writer().Write(w, r, updatedFlow) } diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index b3dae8a6a1c5..2786a03fdc79 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -98,6 +98,8 @@ type Flow struct { OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"` // TransientPayload is used to pass data from the registration to a webhook + // + // required: false TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` // Contains a list of actions, that could follow this flow @@ -269,3 +271,7 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go index a96da053d766..25632cd2e93e 100644 --- a/selfservice/flow/settings/flow.go +++ b/selfservice/flow/settings/flow.go @@ -119,6 +119,11 @@ type Flow struct { // // required: false ContinueWithItems []flow.ContinueWith `json:"continue_with,omitempty" db:"-" faker:"-" ` + + // TransientPayload is used to pass data from the settings flow to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` } var _ flow.Flow = new(Flow) @@ -256,3 +261,7 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go index 264e356da476..a0de0250b54d 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -92,6 +92,11 @@ type Flow struct { // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` NID uuid.UUID `json:"-" faker:"-" db:"nid"` + + // TransientPayload is used to pass data from the verification flow to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` } type OAuth2LoginChallengeParams struct { @@ -288,3 +293,7 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index fbdbb1f79b88..c3d8344fc7a8 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -48,6 +48,13 @@ import ( "github.com/ory/x/snapshotx" ) +var transientPayload = json.RawMessage(`{ + "stuff": { + "name": "fubar", + "numbers": [42, 12345, 3.1415] + } +}`) + func TestWebHooks(t *testing.T) { _, reg := internal.NewFastRegistryWithMocks(t) logger := logrusx.New("kratos", "test") @@ -112,8 +119,8 @@ func TestWebHooks(t *testing.T) { bodyWithFlowOnly := func(req *http.Request, f flow.Flow) string { h, _ := json.Marshal(req.Header) return fmt.Sprintf(`{ - "flow_id": "%s", - "headers": %s, + "flow_id": "%s", + "headers": %s, "method": "%s", "url": "%s", "cookies": { @@ -124,28 +131,12 @@ func TestWebHooks(t *testing.T) { }`, f.GetID(), string(h), req.Method, "http://www.ory.sh/some_end_point") } - bodyWithFlowAndIdentity := func(req *http.Request, f flow.Flow, s *session.Session) string { - h, _ := json.Marshal(req.Header) - return fmt.Sprintf(`{ - "flow_id": "%s", - "identity_id": "%s", - "headers": %s, - "method": "%s", - "url": "%s", - "cookies": { - "Some-Cookie-1": "Some-Cookie-Value", - "Some-Cookie-2": "Some-other-Cookie-Value", - "Some-Cookie-3": "Third-Cookie-Value" - } - }`, f.GetID(), s.Identity.ID, string(h), req.Method, "http://www.ory.sh/some_end_point") - } - bodyWithFlowAndIdentityAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string { h, _ := json.Marshal(req.Header) return fmt.Sprintf(`{ - "flow_id": "%s", + "flow_id": "%s", "identity_id": "%s", - "headers": %s, + "headers": %s, "method": "%s", "url": "%s", "cookies": { @@ -175,12 +166,12 @@ func TestWebHooks(t *testing.T) { }, { uc: "Post Login Hook", - createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, { @@ -197,55 +188,45 @@ func TestWebHooks(t *testing.T) { uc: "Post Registration Hook", createFlow: func() flow.Flow { return ®istration.Flow{ - ID: x.NewUUID(), - TransientPayload: json.RawMessage(`{ - "stuff": { - "name": "fubar", - "numbers": [42, 12345, 3.1415] - } - }`), + ID: x.NewUUID(), + TransientPayload: transientPayload, } }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, json.RawMessage(`{ - "stuff": { - "name": "fubar", - "numbers": [42, 12345, 3.1415] - } - }`)) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, { uc: "Post Recovery Hook", - createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} }, + createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, { uc: "Post Verification Hook", - createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} }, + createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecutePostVerificationHook(nil, req, f.(*verification.Flow), s.Identity) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, { uc: "Post Settings Hook", - createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} }, + createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity, s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, } { diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json index fa030cbd67a0..1bcc36b12c88 100644 --- a/selfservice/strategy/code/.schema/login.schema.json +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -27,6 +27,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/.schema/recovery.schema.json b/selfservice/strategy/code/.schema/recovery.schema.json index 37b6f11e7f06..011503132baa 100644 --- a/selfservice/strategy/code/.schema/recovery.schema.json +++ b/selfservice/strategy/code/.schema/recovery.schema.json @@ -23,6 +23,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/.schema/verification.schema.json b/selfservice/strategy/code/.schema/verification.schema.json index 107d331972b1..e60989dcf52e 100644 --- a/selfservice/strategy/code/.schema/verification.schema.json +++ b/selfservice/strategy/code/.schema/verification.schema.json @@ -23,6 +23,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index 479dfaad09bc..fdc5e8b38b2d 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -69,6 +69,11 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit WithSensitiveField("address", addresses). Debugf("Preparing %s code", f.GetFlowName()) + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + // send to all addresses for _, address := range addresses { // We have to generate a unique code per address, or otherwise it is not possible to link which @@ -101,6 +106,7 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit RegistrationCode: rawCode, Traits: model, RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, } s.deps.Audit(). @@ -142,17 +148,19 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit switch address.Via { case identity.ChannelTypeEmail: t = email.NewLoginCodeValid(s.deps, &email.LoginCodeValidModel{ - To: address.To, - LoginCode: rawCode, - Identity: model, - RequestURL: f.GetRequestURL(), + To: address.To, + LoginCode: rawCode, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) case identity.ChannelTypeSMS: t = sms.NewLoginCodeValid(s.deps, &sms.LoginCodeValidModel{ - To: address.To, - LoginCode: rawCode, - Identity: model, - RequestURL: f.GetRequestURL(), + To: address.To, + LoginCode: rawCode, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) } @@ -188,11 +196,17 @@ func (s *Sender) SendRecoveryCode(ctx context.Context, f *recovery.Flow, via ide WithField("strategy", "code"). WithField("was_notified", notifyUnknownRecipients). Info("Account recovery was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewRecoveryCodeInvalid(s.deps, &email.RecoveryCodeInvalidModel{ - To: to, - RequestURL: f.RequestURL, + To: to, + RequestURL: f.RequestURL, + TransientPayload: transientPayload, })); err != nil { return err } @@ -227,7 +241,7 @@ func (s *Sender) SendRecoveryCode(ctx context.Context, f *recovery.Flow, via ide return s.SendRecoveryCodeTo(ctx, i, rawCode, code, f) } -func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, codeString string, code *RecoveryCode, f flow.Flow) error { +func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, codeString string, code *RecoveryCode, f *recovery.Flow) error { s.deps.Audit(). WithField("via", code.RecoveryAddress.Via). WithField("identity_id", code.RecoveryAddress.IdentityID). @@ -241,11 +255,17 @@ func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, c return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + emailModel := email.RecoveryCodeValidModel{ - To: code.RecoveryAddress.Value, - RecoveryCode: codeString, - Identity: model, - RequestURL: f.GetRequestURL(), + To: code.RecoveryAddress.Value, + RecoveryCode: codeString, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, } return s.send(ctx, string(code.RecoveryAddress.Via), email.NewRecoveryCodeValid(s.deps, &emailModel)) @@ -271,11 +291,17 @@ func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, WithSensitiveField("email_address", to). WithField("was_notified", notifyUnknownRecipients). Info("Address verification was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewVerificationCodeInvalid(s.deps, &email.VerificationCodeInvalidModel{ - To: to, - RequestURL: f.GetRequestURL(), + To: to, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } @@ -302,10 +328,7 @@ func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, return err } - if err := s.SendVerificationCodeTo(ctx, f, i, rawCode, code); err != nil { - return err - } - return nil + return s.SendVerificationCodeTo(ctx, f, i, rawCode, code) } func (s *Sender) constructVerificationLink(ctx context.Context, fID uuid.UUID, codeStr string) string { @@ -331,6 +354,11 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + var t courier.Template // TODO: this can likely be abstracted by making templates not specific to the channel they're using @@ -342,6 +370,7 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo Identity: model, VerificationCode: codeString, RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) case identity.ChannelTypeSMS: t = sms.NewVerificationCodeValid(s.deps, &sms.VerificationCodeValidModel{ @@ -349,6 +378,7 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo VerificationCode: codeString, Identity: model, RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) default: return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected email or sms but got %s", code.VerifiableAddress.Via)) diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index ac658e238dcb..a9d7459f5c56 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -6,6 +6,7 @@ package code import ( "context" "database/sql" + "encoding/json" "net/http" "strings" @@ -57,6 +58,11 @@ type updateLoginFlowWithCodeMethod struct { // Resend is set when the user wants to resend the code // required: false Resend string `json:"resend" form:"resend"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) {} @@ -173,6 +179,8 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.HandleLoginError(r, f, &p, err) } + f.TransientPayload = p.TransientPayload + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.HandleLoginError(r, f, &p, err) } diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 6a64f82e876a..45f9a19e74fc 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -4,6 +4,7 @@ package code import ( + "encoding/json" "net/http" "net/url" "time" @@ -83,6 +84,11 @@ type updateRecoveryFlowWithCodeMethod struct { // // required: true Method recovery.RecoveryMethod `json:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s Strategy) isCodeFlow(f *recovery.Flow) bool { @@ -107,6 +113,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return s.HandleRecoveryError(w, r, nil, body, err) } + f.TransientPayload = body.TransientPayload + if f.DangerousSkipCSRFCheck { s.deps.Logger(). WithRequest(r). @@ -460,11 +468,12 @@ func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, f } type recoverySubmitPayload struct { - Method string `json:"method" form:"method"` - Code string `json:"code" form:"code"` - CSRFToken string `json:"csrf_token" form:"csrf_token"` - Flow string `json:"flow" form:"flow"` - Email string `json:"email" form:"email"` + Method string `json:"method" form:"method"` + Code string `json:"code" form:"code"` + CSRFToken string `json:"csrf_token" form:"csrf_token"` + Flow string `json:"flow" form:"flow"` + Email string `json:"email" form:"email"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) decodeRecovery(r *http.Request) (*recoverySubmitPayload, error) { diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index 4d728426348f..f9da6d29fcb5 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -50,15 +50,15 @@ type updateRegistrationFlowWithCodeMethod struct { // required: true Method string `json:"method" form:"method"` - // Transient data to pass along to any webhooks + // Resend restarts the flow with a new code // // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` + Resend string `json:"resend" form:"resend"` - // Resend restarts the flow with a new code + // Transient data to pass along to any webhooks // // required: false - Resend string `json:"resend" form:"resend"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateRegistrationFlowWithCodeMethod) GetResend() string { @@ -99,7 +99,7 @@ func WithCredentials(via identity.CodeAddressType, usedAt sql.NullTime) options } } -func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flow, traits json.RawMessage, transientPayload json.RawMessage, i *identity.Identity, opts ...options) error { +func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flow, traits, transientPayload json.RawMessage, i *identity.Identity, opts ...options) error { f.TransientPayload = transientPayload if len(traits) == 0 { traits = json.RawMessage("{}") @@ -152,6 +152,8 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return s.HandleRegistrationError(ctx, r, f, &p, err) } + f.TransientPayload = p.TransientPayload + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { return s.HandleRegistrationError(ctx, r, f, &p, err) } diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index e6969fda738d..2f80a4490982 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -5,6 +5,7 @@ package code import ( "context" + "encoding/json" "net/http" "time" @@ -110,6 +111,11 @@ type updateVerificationFlowWithCodeMethod struct { // The id of the flow Flow string `json:"-" form:"-"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } // getMethod returns the method of this submission or "" if no method could be found @@ -134,6 +140,8 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio return s.handleVerificationError(w, r, nil, body, err) } + f.TransientPayload = body.TransientPayload + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil { return s.handleVerificationError(w, r, f, body, err) } diff --git a/selfservice/strategy/link/.schema/recovery.schema.json b/selfservice/strategy/link/.schema/recovery.schema.json index e633741bc15b..cc4439c11351 100644 --- a/selfservice/strategy/link/.schema/recovery.schema.json +++ b/selfservice/strategy/link/.schema/recovery.schema.json @@ -19,6 +19,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/link/.schema/verification.schema.json b/selfservice/strategy/link/.schema/verification.schema.json index e633741bc15b..cc4439c11351 100644 --- a/selfservice/strategy/link/.schema/verification.schema.json +++ b/selfservice/strategy/link/.schema/verification.schema.json @@ -19,6 +19,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/link/sender.go b/selfservice/strategy/link/sender.go index 5ecd27bdcf10..41231a721b8b 100644 --- a/selfservice/strategy/link/sender.go +++ b/selfservice/strategy/link/sender.go @@ -73,11 +73,17 @@ func (s *Sender) SendRecoveryLink(ctx context.Context, f *recovery.Flow, via ide WithSensitiveField("email_address", address). WithField("was_notified", notifyUnknownRecipients). Info("Account recovery was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewRecoveryInvalid(s.r, &email.RecoveryInvalidModel{ - To: to, - RequestURL: f.GetRequestURL(), + To: to, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } @@ -125,11 +131,17 @@ func (s *Sender) SendVerificationLink(ctx context.Context, f *verification.Flow, WithSensitiveField("email_address", to). WithField("was_notified", notifyUnknownRecipients). Info("Address verification was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewVerificationInvalid(s.r, &email.VerificationInvalidModel{ - To: to, - RequestURL: f.GetRequestURL(), + To: to, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } @@ -170,15 +182,26 @@ func (s *Sender) SendRecoveryTokenTo(ctx context.Context, f *recovery.Flow, i *i return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + recoveryUrl := urlx.CopyWithQuery( + urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), recovery.RouteSubmitFlow), + url.Values{ + "token": {token.Token}, + "flow": {f.ID.String()}, + }). + String() + return s.send(ctx, string(address.Via), email.NewRecoveryValid(s.r, - &email.RecoveryValidModel{To: address.Value, RecoveryURL: urlx.CopyWithQuery( - urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), recovery.RouteSubmitFlow), - url.Values{ - "token": {token.Token}, - "flow": {f.ID.String()}, - }).String(), - Identity: model, - RequestURL: f.GetRequestURL(), + &email.RecoveryValidModel{ + To: address.Value, + RecoveryURL: recoveryUrl, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })) } @@ -196,17 +219,25 @@ func (s *Sender) SendVerificationTokenTo(ctx context.Context, f *verification.Fl return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + verificationUrl := urlx.CopyWithQuery( + urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), verification.RouteSubmitFlow), + url.Values{ + "flow": {f.ID.String()}, + "token": {token.Token}, + }).String() + if err := s.send(ctx, string(address.Via), email.NewVerificationValid(s.r, &email.VerificationValidModel{ - To: address.Value, - VerificationURL: urlx.CopyWithQuery( - urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), verification.RouteSubmitFlow), - url.Values{ - "flow": {f.ID.String()}, - "token": {token.Token}, - }).String(), - Identity: model, - RequestURL: f.GetRequestURL(), + To: address.Value, + VerificationURL: verificationUrl, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 6297e780b646..c8be6025f840 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -4,6 +4,7 @@ package link import ( + "encoding/json" "net/http" "net/url" "time" @@ -232,6 +233,11 @@ type updateRecoveryFlowWithLinkMethod struct { // // required: true Method recovery.RecoveryMethod `json:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.Flow) (err error) { @@ -244,6 +250,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return s.HandleRecoveryError(w, r, nil, body, err) } + f.TransientPayload = body.TransientPayload + if len(body.Token) > 0 { if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) @@ -514,11 +522,12 @@ func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, r } type recoverySubmitPayload struct { - Method string `json:"method" form:"method"` - Token string `json:"token" form:"token"` - CSRFToken string `json:"csrf_token" form:"csrf_token"` - Flow string `json:"flow" form:"flow"` - Email string `json:"email" form:"email"` + Method string `json:"method" form:"method"` + Token string `json:"token" form:"token"` + CSRFToken string `json:"csrf_token" form:"csrf_token"` + Flow string `json:"flow" form:"flow"` + Email string `json:"email" form:"email"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) decodeRecovery(r *http.Request) (*recoverySubmitPayload, error) { diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index 0db56233fa3f..6fe054f746fb 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -5,6 +5,7 @@ package link import ( "context" + "encoding/json" "net/http" "net/url" "time" @@ -48,11 +49,12 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F } type verificationSubmitPayload struct { - Method string `json:"method" form:"method"` - Token string `json:"token" form:"token"` - CSRFToken string `json:"csrf_token" form:"csrf_token"` - Flow string `json:"flow" form:"flow"` - Email string `json:"email" form:"email"` + Method string `json:"method" form:"method"` + Token string `json:"token" form:"token"` + CSRFToken string `json:"csrf_token" form:"csrf_token"` + Flow string `json:"flow" form:"flow"` + Email string `json:"email" form:"email"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) decodeVerification(r *http.Request) (*verificationSubmitPayload, error) { @@ -115,6 +117,11 @@ type updateVerificationFlowWithLinkMethod struct { // // required: true Method verification.VerificationStrategy `json:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verification.Flow) (err error) { @@ -127,6 +134,7 @@ func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verificatio if err != nil { return s.handleVerificationError(w, r, nil, body, err) } + f.TransientPayload = body.TransientPayload if len(body.Token) > 0 { if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { diff --git a/selfservice/strategy/lookup/.schema/login.schema.json b/selfservice/strategy/lookup/.schema/login.schema.json index e2af475932d3..763757813b45 100644 --- a/selfservice/strategy/lookup/.schema/login.schema.json +++ b/selfservice/strategy/lookup/.schema/login.schema.json @@ -15,6 +15,10 @@ }, "lookup_secret": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/lookup/.schema/settings.schema.json b/selfservice/strategy/lookup/.schema/settings.schema.json index 729a9bb1f5e8..d20cdb77dd32 100644 --- a/selfservice/strategy/lookup/.schema/settings.schema.json +++ b/selfservice/strategy/lookup/.schema/settings.schema.json @@ -20,6 +20,10 @@ }, "lookup_secret_confirm": { "type": "boolean" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/lookup/settings.go b/selfservice/strategy/lookup/settings.go index 6f61966e353c..1136d4d83414 100644 --- a/selfservice/strategy/lookup/settings.go +++ b/selfservice/strategy/lookup/settings.go @@ -82,6 +82,11 @@ type updateSettingsFlowWithLookupMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithLookupMethod) GetFlowID() uuid.UUID { diff --git a/selfservice/strategy/oidc/.schema/settings.schema.json b/selfservice/strategy/oidc/.schema/settings.schema.json index 2eee443b9ca6..0c9dae1c6460 100644 --- a/selfservice/strategy/oidc/.schema/settings.schema.json +++ b/selfservice/strategy/oidc/.schema/settings.schema.json @@ -33,6 +33,10 @@ }, "additionalProperties": false } + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index be3c6762e3e3..fe90eab8ad4e 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -100,6 +100,11 @@ type UpdateLoginFlowWithOidcMethod struct { // // required: false IDTokenNonce string `json:"id_token_nonce,omitempty"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *AuthCodeContainer) (*registration.Flow, error) { @@ -146,6 +151,7 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlo registrationFlow.IDToken = loginFlow.IDToken registrationFlow.RawIDTokenNonce = loginFlow.RawIDTokenNonce registrationFlow.RequestURL, err = x.TakeOverReturnToParameter(loginFlow.RequestURL, registrationFlow.RequestURL) + registrationFlow.TransientPayload = loginFlow.TransientPayload if err != nil { return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } @@ -195,6 +201,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, f.IDToken = p.IDToken f.RawIDTokenNonce = p.IDTokenNonce + f.TransientPayload = p.TransientPayload pid := p.Provider // this can come from both url query and post body if pid == "" { diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index ca8a83be8c10..f16ba63f07b4 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -84,11 +84,6 @@ type UpdateRegistrationFlowWithOidcMethod struct { // required: true Method string `json:"method"` - // Transient data to pass along to any webhooks - // - // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty"` - // UpstreamParameters are the parameters that are passed to the upstream identity provider. // // These parameters are optional and depend on what the upstream identity provider supports. @@ -117,6 +112,11 @@ type UpdateRegistrationFlowWithOidcMethod struct { // // required: false IDTokenNonce string `json:"id_token_nonce,omitempty"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { @@ -157,15 +157,15 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return s.handleError(w, r, f, "", nil, err) } - f.TransientPayload = p.TransientPayload - f.IDToken = p.IDToken - f.RawIDTokenNonce = p.IDTokenNonce - pid := p.Provider // this can come from both url query and post body if pid == "" { return errors.WithStack(flow.ErrStrategyNotResponsible) } + f.TransientPayload = p.TransientPayload + f.IDToken = p.IDToken + f.RawIDTokenNonce = p.IDTokenNonce + if !strings.EqualFold(strings.ToLower(p.Method), s.SettingsStrategyID()) && p.Method != "" { // the user is sending a method that is not oidc, but the payload includes a provider s.d.Audit(). @@ -274,6 +274,7 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r if err != nil { return nil, err } + lf.TransientPayload = rf.TransientPayload return lf, nil } diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 24623938fd87..ed94972b1500 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -38,13 +38,20 @@ import ( //go:embed .schema/settings.schema.json var settingsSchema []byte -var _ settings.Strategy = new(Strategy) -var UnknownConnectionValidationError = &jsonschema.ValidationError{ - Message: "can not unlink non-existing OpenID Connect connection", InstancePtr: "#/"} +var ( + _ settings.Strategy = new(Strategy) + UnknownConnectionValidationError = &jsonschema.ValidationError{ + Message: "can not unlink non-existing OpenID Connect connection", InstancePtr: "#/", + } +) + var ConnectionExistValidationError = &jsonschema.ValidationError{ - Message: "can not link unknown or already existing OpenID Connect connection", InstancePtr: "#/"} + Message: "can not link unknown or already existing OpenID Connect connection", InstancePtr: "#/", +} + var UnlinkAllFirstFactorConnectionsError = &jsonschema.ValidationError{ - Message: "can not unlink OpenID Connect connection because it is the last remaining first factor credential", InstancePtr: "#/"} + Message: "can not unlink OpenID Connect connection because it is the last remaining first factor credential", InstancePtr: "#/", +} func (s *Strategy) RegisterSettingsRoutes(router *x.RouterPublic) {} @@ -237,6 +244,11 @@ type updateSettingsFlowWithOidcMethod struct { // // required: false UpstreamParameters json.RawMessage `json:"upstream_parameters"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithOidcMethod) GetFlowID() uuid.UUID { @@ -252,6 +264,7 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. if err := s.decoderSettings(&p, r); err != nil { return nil, err } + f.TransientPayload = p.TransientPayload ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) if errors.Is(err, settings.ErrContinuePreviousAction) { @@ -306,7 +319,8 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. return nil, s.handleSettingsError(w, r, ctxUpdate, &p, errors.WithStack(errors.WithStack(&jsonschema.ValidationError{ Message: "missing properties: link, unlink", InstancePtr: "#/", - Context: &jsonschema.ValidationErrorContextRequired{Missing: []string{"link", "unlink"}}}))) + Context: &jsonschema.ValidationErrorContextRequired{Missing: []string{"link", "unlink"}}, + }))) } func (s *Strategy) isLinkable(r *http.Request, ctxUpdate *settings.UpdateContext, toLink string) (*identity.Identity, error) { @@ -391,7 +405,8 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *oauth2.Token, claims *Claims, provider Provider) error { p := &updateSettingsFlowWithOidcMethod{ - Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String()} + Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String(), + } if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) } @@ -490,7 +505,6 @@ func (s *Strategy) unlinkProvider(w http.ResponseWriter, r *http.Request, ctxUpd creds.Config, err = json.Marshal(&identity.CredentialsOIDC{Providers: updatedProviders}) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(err)) - } i.Credentials[s.ID()] = *creds @@ -527,7 +541,7 @@ func (s *Strategy) Link(ctx context.Context, i *identity.Identity, credentialsCo if len(credentialsOIDCConfig.Providers) != 1 { return errors.New("No oidc provider was set") } - var credentialsOIDCProvider = credentialsOIDCConfig.Providers[0] + credentialsOIDCProvider := credentialsOIDCConfig.Providers[0] if err := s.linkCredentials( ctx, diff --git a/selfservice/strategy/password/.schema/login.schema.json b/selfservice/strategy/password/.schema/login.schema.json index 0ceccbc72738..67b1252c0ec0 100644 --- a/selfservice/strategy/password/.schema/login.schema.json +++ b/selfservice/strategy/password/.schema/login.schema.json @@ -16,6 +16,10 @@ }, "method": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "allOf": [ diff --git a/selfservice/strategy/password/.schema/settings.schema.json b/selfservice/strategy/password/.schema/settings.schema.json index 442efe38b2a7..5e7b66784a69 100644 --- a/selfservice/strategy/password/.schema/settings.schema.json +++ b/selfservice/strategy/password/.schema/settings.schema.json @@ -15,6 +15,10 @@ "password": { "type": "string", "minLength": 1 + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 706a4fcab0f0..8c91d7e6c4f9 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -63,6 +63,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { return nil, s.handleLoginError(w, r, f, &p, err) } + f.TransientPayload = p.TransientPayload if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.handleLoginError(w, r, f, &p, err) diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 7dc7a627ffab..32b090cf1183 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -50,7 +50,7 @@ type UpdateRegistrationFlowWithPasswordMethod struct { // Transient data to pass along to any webhooks // // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) RegisterRegistrationRoutes(_ *x.RouterPublic) { diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index f5447ab44eb2..f763163d3180 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -56,6 +56,11 @@ type updateSettingsFlowWithPasswordMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithPasswordMethod) GetFlowID() uuid.UUID { diff --git a/selfservice/strategy/password/types.go b/selfservice/strategy/password/types.go index 61a4f73cb4ae..6a3515c1a3dc 100644 --- a/selfservice/strategy/password/types.go +++ b/selfservice/strategy/password/types.go @@ -3,9 +3,7 @@ package password -import ( - "github.com/ory/kratos/ui/container" -) +import "encoding/json" // Update Login Flow with Password Method // @@ -32,9 +30,9 @@ type updateLoginFlowWithPasswordMethod struct { // // required: true Identifier string `json:"identifier"` -} -// FlowMethod contains the configuration for this selfservice strategy. -type FlowMethod struct { - *container.Container + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } diff --git a/selfservice/strategy/profile/.schema/settings.schema.json b/selfservice/strategy/profile/.schema/settings.schema.json index 4241cbdc22bb..5ae4ad70d94a 100644 --- a/selfservice/strategy/profile/.schema/settings.schema.json +++ b/selfservice/strategy/profile/.schema/settings.schema.json @@ -12,6 +12,10 @@ "traits": {}, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go index 5b8d7f368306..669d4c0e8d5d 100644 --- a/selfservice/strategy/profile/strategy.go +++ b/selfservice/strategy/profile/strategy.go @@ -208,6 +208,11 @@ type updateSettingsFlowWithProfileMethod struct { // // This token is only required when performing browser flows. CSRFToken string `json:"csrf_token"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithProfileMethod) GetFlowID() uuid.UUID { diff --git a/selfservice/strategy/totp/.schema/login.schema.json b/selfservice/strategy/totp/.schema/login.schema.json index 2ef6aeaf9a9d..fb32f6a08f1f 100644 --- a/selfservice/strategy/totp/.schema/login.schema.json +++ b/selfservice/strategy/totp/.schema/login.schema.json @@ -16,6 +16,10 @@ "totp_code": { "type": "string", "minLength": 6 + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/totp/.schema/settings.schema.json b/selfservice/strategy/totp/.schema/settings.schema.json index cab5d763b33a..8767f5a08f41 100644 --- a/selfservice/strategy/totp/.schema/settings.schema.json +++ b/selfservice/strategy/totp/.schema/settings.schema.json @@ -14,6 +14,10 @@ }, "totp_unlink": { "type": "boolean" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "if": { diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go index 1eaa5aeedac7..2aaface8dc5c 100644 --- a/selfservice/strategy/totp/login.go +++ b/selfservice/strategy/totp/login.go @@ -83,6 +83,11 @@ type updateLoginFlowWithTotpMethod struct { // // required: true TOTPCode string `json:"totp_code"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) { @@ -101,6 +106,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { return nil, s.handleLoginError(r, f, err) } + f.TransientPayload = p.TransientPayload if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.handleLoginError(r, f, err) diff --git a/selfservice/strategy/totp/settings.go b/selfservice/strategy/totp/settings.go index 0587e87e2d6a..bbb3f5496dc5 100644 --- a/selfservice/strategy/totp/settings.go +++ b/selfservice/strategy/totp/settings.go @@ -68,6 +68,11 @@ type updateSettingsFlowWithTotpMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithTotpMethod) GetFlowID() uuid.UUID { diff --git a/selfservice/strategy/webauthn/.schema/login.schema.json b/selfservice/strategy/webauthn/.schema/login.schema.json index b548b0d9e98e..3029101023ad 100644 --- a/selfservice/strategy/webauthn/.schema/login.schema.json +++ b/selfservice/strategy/webauthn/.schema/login.schema.json @@ -15,6 +15,10 @@ "identifier": { "type": "string", "minLength": 1 + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "if": { diff --git a/selfservice/strategy/webauthn/.schema/settings.schema.json b/selfservice/strategy/webauthn/.schema/settings.schema.json index cca82040fabf..f89ec987e37f 100644 --- a/selfservice/strategy/webauthn/.schema/settings.schema.json +++ b/selfservice/strategy/webauthn/.schema/settings.schema.json @@ -17,6 +17,10 @@ }, "webauthn_remove": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "if": { diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 38cdf283b9f1..6f160929c124 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -199,6 +199,11 @@ type updateLoginFlowWithWebAuthnMethod struct { // // This must contain the ID of the WebAuthN connection. Login string `json:"webauthn_login"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) { @@ -213,6 +218,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { return nil, s.handleLoginError(r, f, err) } + f.TransientPayload = p.TransientPayload if len(p.Login) > 0 || p.Method == s.SettingsStrategyID() { // This method has only two submit buttons diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go index 565125319634..7ca2b3a67301 100644 --- a/selfservice/strategy/webauthn/registration.go +++ b/selfservice/strategy/webauthn/registration.go @@ -64,7 +64,7 @@ type updateRegistrationFlowWithWebAuthnMethod struct { // Transient data to pass along to any webhooks // // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) RegisterRegistrationRoutes(_ *x.RouterPublic) { diff --git a/selfservice/strategy/webauthn/settings.go b/selfservice/strategy/webauthn/settings.go index 51fbcf0f5adb..adbb91e8ae8c 100644 --- a/selfservice/strategy/webauthn/settings.go +++ b/selfservice/strategy/webauthn/settings.go @@ -83,6 +83,11 @@ type updateSettingsFlowWithWebAuthnMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithWebAuthnMethod) GetFlowID() uuid.UUID { diff --git a/spec/api.json b/spec/api.json index 17c3e89f193d..ecb4debbb17d 100644 --- a/spec/api.json +++ b/spec/api.json @@ -1331,6 +1331,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the login to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -1629,6 +1633,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the recovery flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -1989,6 +1997,10 @@ "state": { "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the settings flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -2573,6 +2585,10 @@ "resend": { "description": "Resend is set when the user wants to resend the code", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2630,6 +2646,10 @@ "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "upstream_parameters": { "description": "UpstreamParameters are the parameters that are passed to the upstream identity provider.\n\nThese parameters are optional and depend on what the upstream identity provider supports.\nSupported parameters are:\n`login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session.\n`hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`.\n`prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`.", "type": "object" @@ -2663,6 +2683,10 @@ "password_identifier": { "description": "Identifier is the email or username of the user trying to log in.\nThis field is deprecated!", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2686,6 +2710,10 @@ "totp_code": { "description": "The TOTP code.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2709,6 +2737,10 @@ "description": "Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_login": { "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", "type": "string" @@ -2761,6 +2793,10 @@ ], "type": "string", "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2787,6 +2823,10 @@ ], "type": "string", "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3025,6 +3065,10 @@ "method": { "description": "Method\n\nShould be set to \"lookup\" when trying to add, update, or remove a lookup pairing.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3051,6 +3095,10 @@ "description": "The identity's traits\n\nin: body", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "unlink": { "description": "Unlink this provider\n\nEither this or `link` must be set.\n\ntype: string\nin: body", "type": "string" @@ -3079,6 +3127,10 @@ "password": { "description": "Password is the updated password", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3101,6 +3153,10 @@ "traits": { "description": "Traits\n\nThe identity's traits.", "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3127,6 +3183,10 @@ "totp_unlink": { "description": "UnlinkTOTP if true will remove the TOTP pairing,\neffectively removing the credential. This can be used\nto set up a new TOTP device.", "type": "boolean" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3145,6 +3205,10 @@ "description": "Method\n\nShould be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_register": { "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", "type": "string" @@ -3203,6 +3267,10 @@ ], "type": "string", "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3229,6 +3297,10 @@ ], "type": "string", "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3323,6 +3395,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the verification flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, diff --git a/spec/swagger.json b/spec/swagger.json index 4d97d50bb9b7..0f99a63b0ee7 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -4459,6 +4459,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the login to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -4743,6 +4747,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the recovery flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -5094,6 +5102,10 @@ "state": { "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the settings flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -5612,6 +5624,10 @@ "resend": { "description": "Resend is set when the user wants to resend the code", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5669,6 +5685,10 @@ "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "upstream_parameters": { "description": "UpstreamParameters are the parameters that are passed to the upstream identity provider.\n\nThese parameters are optional and depend on what the upstream identity provider supports.\nSupported parameters are:\n`login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session.\n`hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`.\n`prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`.", "type": "object" @@ -5703,6 +5723,10 @@ "password_identifier": { "description": "Identifier is the email or username of the user trying to log in.\nThis field is deprecated!", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5725,6 +5749,10 @@ "totp_code": { "description": "The TOTP code.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5748,6 +5776,10 @@ "description": "Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_login": { "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", "type": "string" @@ -5785,6 +5817,10 @@ "code" ], "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5812,6 +5848,10 @@ "code" ], "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5994,6 +6034,10 @@ "method": { "description": "Method\n\nShould be set to \"lookup\" when trying to add, update, or remove a lookup pairing.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6020,6 +6064,10 @@ "description": "The identity's traits\n\nin: body", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "unlink": { "description": "Unlink this provider\n\nEither this or `link` must be set.\n\ntype: string\nin: body", "type": "string" @@ -6049,6 +6097,10 @@ "password": { "description": "Password is the updated password", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6071,6 +6123,10 @@ "traits": { "description": "Traits\n\nThe identity's traits.", "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6096,6 +6152,10 @@ "totp_unlink": { "description": "UnlinkTOTP if true will remove the TOTP pairing,\neffectively removing the credential. This can be used\nto set up a new TOTP device.", "type": "boolean" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6114,6 +6174,10 @@ "description": "Method\n\nShould be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_register": { "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", "type": "string" @@ -6158,6 +6222,10 @@ "code" ], "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6185,6 +6253,10 @@ "code" ], "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6282,6 +6354,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the verification flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, diff --git a/test/e2e/cypress/downloads/downloads.html b/test/e2e/cypress/downloads/downloads.html new file mode 100644 index 0000000000000000000000000000000000000000..4e6e883a5f39548b6afb7fccbc6b9fda25d65ef0 GIT binary patch literal 4680 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 By#oLM literal 0 HcmV?d00001 diff --git a/test/e2e/cypress/helpers/webhook.ts b/test/e2e/cypress/helpers/webhook.ts index fdc37b3fbc68..a5da27abae61 100644 --- a/test/e2e/cypress/helpers/webhook.ts +++ b/test/e2e/cypress/helpers/webhook.ts @@ -8,7 +8,7 @@ const WEBHOOK_TARGET = "https://webhook-target-gsmwn5ab4a-uc.a.run.app" const documentUrl = (key: string) => `${WEBHOOK_TARGET}/documents/${key}` const jsonnet = Buffer.from("function(ctx) ctx").toString("base64") -export const testRegistrationWebhook = ( +export const testFlowWebhook = ( configSetup: ( hooks: Array<{ hook: string; config?: any }>, ) => Cypress.Chainable, @@ -24,7 +24,6 @@ export const testRegistrationWebhook = ( method: "PUT", }, }, - { hook: "session" }, ]) const transient_payload = { @@ -34,27 +33,31 @@ export const testRegistrationWebhook = ( }, consent: true, } - cy.intercept("POST", /.*\/self-service\/registration.*/, (req) => { - switch (typeof req.body) { - case "string": - req.body = - req.body + - "&transient_payload=" + - encodeURIComponent(JSON.stringify(transient_payload)) - break - case "object": - req.body = { - ...req.body, - transient_payload, - } - break + cy.intercept( + "POST", + /.*\/self-service\/(registration|login|recovery|verification|settings).*/, + (req) => { + switch (typeof req.body) { + case "string": + req.body = + req.body + + "&transient_payload=" + + encodeURIComponent(JSON.stringify(transient_payload)) + break + case "object": + req.body = { + ...req.body, + transient_payload, + } + break - default: - fail() - break - } - req.continue() - }) + default: + fail() + break + } + req.continue() + }, + ) act() diff --git a/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts index 100bc50ab9c4..2a2d6a1c24d5 100644 --- a/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { gen, MOBILE_URL, website } from "../../../../helpers" -import { testRegistrationWebhook } from "../../../../helpers/webhook" +import { testFlowWebhook } from "../../../../helpers/webhook" context("Mobile Profile", () => { describe("Login Flow Success", () => { @@ -29,8 +29,12 @@ context("Mobile Profile", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "password", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "password", [ + ...hooks, + { hook: "session" }, + ]), () => { const email = gen.email() const password = gen.password() diff --git a/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts index fe32cbbf61a2..370d5f0ff253 100644 --- a/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts @@ -4,7 +4,7 @@ import { appPrefix, gen, website } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" -import { testRegistrationWebhook } from "../../../../helpers/webhook" +import { testFlowWebhook } from "../../../../helpers/webhook" context("Social Sign Up Successes", () => { ;[ @@ -104,8 +104,12 @@ context("Social Sign Up Successes", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "oidc", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "oidc", [ + ...hooks, + { hook: "session" }, + ]), () => { const email = gen.email() cy.registerOidc({ diff --git a/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts b/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts index 4b5bcfbfbdf8..390491fe8f0d 100644 --- a/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts +++ b/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts @@ -4,7 +4,7 @@ import { appPrefix, gen } from "../../../helpers" import { routes as express } from "../../../helpers/express" import { routes as react } from "../../../helpers/react" -import { testRegistrationWebhook } from "../../../helpers/webhook" +import { testFlowWebhook } from "../../../helpers/webhook" const signup = (registration: string, app: string, email = gen.email()) => { cy.visit(registration) @@ -127,8 +127,12 @@ context("Passwordless registration", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "webauthn", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "webauthn", [ + ...hooks, + { hook: "session" }, + ]), () => { signup(registration, app) }, diff --git a/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts b/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts index b346c9475c05..8e9c00e7ecbb 100644 --- a/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts @@ -3,6 +3,7 @@ import { APP_URL, appPrefix, gen, website } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" +import { testFlowWebhook } from "../../../../helpers/webhook" describe("Basic email profile with succeeding login flows with webhooks", () => { const email = gen.email() @@ -43,6 +44,17 @@ describe("Basic email profile with succeeding login flows with webhooks", () => expect(identity.traits.email).to.equal(email) }) }) + + it("should pass transient_payload to webhook", () => { + testFlowWebhook( + (h) => cy.setupHooks("login", "after", "password", h), + () => { + cy.get(`${appPrefix(app)}input[name="identifier"]`).type(email) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + }, + ) + }) }) }) }) diff --git a/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts index a2dcd123923c..ebea31c9c36b 100644 --- a/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts @@ -3,7 +3,7 @@ import { appPrefix, APP_URL, gen } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" -import { testRegistrationWebhook } from "../../../../helpers/webhook" +import { testFlowWebhook } from "../../../../helpers/webhook" context("Registration success with email profile with webhooks", () => { ;[ @@ -86,18 +86,15 @@ context("Registration success with email profile with webhooks", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - cy.setPostPasswordRegistrationHooks.bind(cy), - () => { - const email = gen.email() - const password = gen.password() + testFlowWebhook(cy.setPostPasswordRegistrationHooks.bind(cy), () => { + const email = gen.email() + const password = gen.password() - cy.get('input[name="traits.email"]').type(email) - cy.get('input[name="password"]').type(password) + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) - cy.submitPasswordForm() - }, - ) + cy.submitPasswordForm() + }) }) it("should sign up and modify the identity", () => { diff --git a/x/json_marshal.go b/x/json_marshal.go new file mode 100644 index 000000000000..65f2ef089bd1 --- /dev/null +++ b/x/json_marshal.go @@ -0,0 +1,18 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import "encoding/json" + +// ParseRawMessageOrEmpty parses a json.RawMessage and returns an empty map if the input is empty. +func ParseRawMessageOrEmpty(input json.RawMessage) (map[string]interface{}, error) { + if len(input) == 0 { + return map[string]interface{}{}, nil + } + var m map[string]interface{} + if err := json.Unmarshal(input, &m); err != nil { + return nil, err + } + return m, nil +} diff --git a/x/json_marshal_test.go b/x/json_marshal_test.go new file mode 100644 index 000000000000..4484699a3fdf --- /dev/null +++ b/x/json_marshal_test.go @@ -0,0 +1,52 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func TestParseRawMessageOrEmpty(t *testing.T) { + for _, tc := range []struct { + input json.RawMessage + expect map[string]interface{} + err any + }{ + { + input: json.RawMessage("invalid json"), + err: "invalid character 'i' looking for beginning of value", + }, + { + input: json.RawMessage(""), + expect: map[string]interface{}{}, + }, + { + input: json.RawMessage(`{"foo": "bar"}`), + expect: map[string]interface{}{ + "foo": "bar", + }, + }, + { + input: json.RawMessage(`{"foo": "b`), + err: "unexpected end of JSON input", + }, + } { + t.Run(fmt.Sprintf("with input '%s'", tc.input), func(t *testing.T) { + m, err := x.ParseRawMessageOrEmpty(tc.input) + if tc.err != nil { + require.Error(t, err) + require.Equal(t, tc.err, err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tc.expect, m) + }) + } +} From c5f39f4bc481e400f736ede7f8f0be546a55eebf Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:51:23 +0100 Subject: [PATCH 006/262] fix: prevent SMTP URL leak on unparsable URL (#3770) --- courier/smtp.go | 4 +++- courier/smtp_test.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/courier/smtp.go b/courier/smtp.go index 8c7ef91fbe33..2aa232132930 100644 --- a/courier/smtp.go +++ b/courier/smtp.go @@ -12,6 +12,8 @@ import ( "strconv" "time" + "github.com/pkg/errors" + "github.com/ory/herodot" "github.com/ory/kratos/driver/config" @@ -27,7 +29,7 @@ type SMTPClient struct { func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, error) { uri, err := url.Parse(cfg.ConnectionURI) if err != nil { - return nil, herodot.ErrInternalServerError.WithError(err.Error()) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The SMTP connection URI is malformed. Please contact a system administrator.")) } var tlsCertificates []tls.Certificate diff --git a/courier/smtp_test.go b/courier/smtp_test.go index 107ab2803447..28271d808179 100644 --- a/courier/smtp_test.go +++ b/courier/smtp_test.go @@ -35,6 +35,23 @@ import ( gomail "github.com/ory/mail/v3" ) +func TestNewSMTPClientPreventLeak(t *testing.T) { + // Test for https://hackerone.com/reports/2384028 + + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + + invalidURL := "sm<>t>p://f%oo::bar:baz@my-server:1234:122/" + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, invalidURL) + channels, err := conf.CourierChannels(ctx) + require.NoError(t, err) + require.Len(t, channels, 1) + + _, err = courier.NewSMTPClient(reg, channels[0].SMTPConfig) + require.Error(t, err) + assert.NotContains(t, err.Error(), invalidURL) +} + func TestNewSMTP(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) From c905f02473c5d77ab309a45f10251b1ba7e88584 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:30:42 +0100 Subject: [PATCH 007/262] fix: add missing indexes and remove unused index (#3756) --- internal/client-go/go.sum | 1 + ...20240214113828000000_courier_dispatch_indices.down.sql | 4 ++++ ...14113828000000_courier_dispatch_indices.mysql.down.sql | 6 ++++++ ...0214113828000000_courier_dispatch_indices.mysql.up.sql | 8 ++++++++ .../20240214113828000000_courier_dispatch_indices.up.sql | 8 ++++++++ 5 files changed, 27 insertions(+) create mode 100644 persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.down.sql create mode 100644 persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.down.sql create mode 100644 persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.up.sql create mode 100644 persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.up.sql diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.down.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.down.sql new file mode 100644 index 000000000000..352396fc888d --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.down.sql @@ -0,0 +1,4 @@ +CREATE INDEX IF NOT EXISTS courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches (id ASC, message_id ASC, nid ASC); + +DROP INDEX courier_message_dispatches_message_id_idx; +DROP INDEX courier_message_dispatches_nid_idx; diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.down.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.down.sql new file mode 100644 index 000000000000..7e45f3f3d284 --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.down.sql @@ -0,0 +1,6 @@ +CREATE INDEX courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches (id ASC, message_id ASC, nid ASC); + +-- These can't be removed because of foreign key constraints which disallow index deletion in MySQL. + +-- DROP INDEX courier_message_dispatches_message_id_idx ON courier_message_dispatches; +-- DROP INDEX courier_message_dispatches_nid_idx ON courier_message_dispatches; diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.up.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.up.sql new file mode 100644 index 000000000000..c277f4dc5c19 --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.up.sql @@ -0,0 +1,8 @@ +-- Remove unused index +DROP INDEX courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches; + +-- For pop eager load +CREATE INDEX courier_message_dispatches_message_id_idx ON courier_message_dispatches (message_id, created_at DESC); + +-- For delete by nid +CREATE INDEX courier_message_dispatches_nid_idx ON courier_message_dispatches (nid); diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.up.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.up.sql new file mode 100644 index 000000000000..46aa3a591eee --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.up.sql @@ -0,0 +1,8 @@ +-- Remove unused index +DROP INDEX courier_message_dispatches_id_message_id_nid_idx; + +-- For pop eager load +CREATE INDEX IF NOT EXISTS courier_message_dispatches_message_id_idx ON courier_message_dispatches (message_id, created_at DESC); + +-- For delete by nid +CREATE INDEX IF NOT EXISTS courier_message_dispatches_nid_idx ON courier_message_dispatches (nid); From 087748c0651ff0fc93259f7ab6b10668c09f5eba Mon Sep 17 00:00:00 2001 From: Kevin Osborn Date: Thu, 22 Feb 2024 03:30:49 -0600 Subject: [PATCH 008/262] Remove unnecessary COPY command from Dockerfile (#3771) --- .docker/Dockerfile-build | 1 - 1 file changed, 1 deletion(-) diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build index e50fc2104570..6ee16085cf84 100644 --- a/.docker/Dockerfile-build +++ b/.docker/Dockerfile-build @@ -9,7 +9,6 @@ WORKDIR /go/src/github.com/ory/kratos COPY go.mod go.mod COPY go.sum go.sum -COPY internal/httpclient/go.* internal/httpclient/ COPY internal/client-go/go.* internal/client-go/ ENV GO111MODULE on From 037bdf82d91651c945bc8e4a40b782a6274ec6b8 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:32:19 +0000 Subject: [PATCH 009/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From b685fa5477be2ba099fd2420b27b2411fafc7e51 Mon Sep 17 00:00:00 2001 From: Oleksandra Talalaieva <25621530+sashatalalasha@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:04:55 +0100 Subject: [PATCH 010/262] fix: add login succeeded event to post registration hook (#3739) --- selfservice/hook/session_issuer.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/selfservice/hook/session_issuer.go b/selfservice/hook/session_issuer.go index 06e8f0e59693..4150fdeffdec 100644 --- a/selfservice/hook/session_issuer.go +++ b/selfservice/hook/session_issuer.go @@ -7,8 +7,11 @@ import ( "context" "net/http" + "go.opentelemetry.io/otel/trace" + "github.com/ory/kratos/identity" "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/events" "github.com/pkg/errors" @@ -21,9 +24,7 @@ import ( "github.com/ory/x/otelx" ) -var ( - _ registration.PostHookPostPersistExecutor = new(SessionIssuer) -) +var _ registration.PostHookPostPersistExecutor = new(SessionIssuer) type ( sessionIssuerDependencies interface { @@ -70,6 +71,14 @@ func (e *SessionIssuer) executePostRegistrationPostPersistHook(w http.ResponseWr Identity: s.Identity, ContinueWith: a.ContinueWithItems, }) + + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ + SessionID: s.ID, + IdentityID: s.Identity.ID, + FlowType: string(a.Type), + Method: a.Active.String(), + })) + return errors.WithStack(registration.ErrHookAbortFlow) } @@ -78,6 +87,13 @@ func (e *SessionIssuer) executePostRegistrationPostPersistHook(w http.ResponseWr return err } + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ + SessionID: s.ID, + IdentityID: s.Identity.ID, + FlowType: string(a.Type), + Method: a.Active.String(), + })) + // SPA flows additionally send the session if x.IsJSONRequest(r) { e.r.Writer().Write(w, r, ®istration.APIFlowResponse{ From 6d7372ee3d88ee4fc552b969dd0ff338dcc0544c Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:14:06 +0100 Subject: [PATCH 011/262] fix: add missing indexes and remove unused index --- internal/client-go/go.sum | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From b7e5144896d14f824be67d5d85f8ce00a38ba14d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:15:58 +0000 Subject: [PATCH 012/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 8f5192fbb74c4b952029a6856284de8d59027770 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 22 Feb 2024 11:28:43 +0100 Subject: [PATCH 013/262] fix: ignore decrypt errors in WithDeclassifiedCredentials (#3731) --- cipher/chacha20.go | 2 +- driver/registry_default.go | 2 +- ...Credentials-case=oidc-credential=oidc.json | 22 ++++++++ ...entials-case=oidc-credential=password.json | 10 ++++ ...entials-case=oidc-credential=webauthn.json | 10 ++++ identity/handler_test.go | 48 +++++++++++++---- identity/identity.go | 51 +++++++++---------- identity/identity_test.go | 27 ++++++++-- 8 files changed, 131 insertions(+), 41 deletions(-) create mode 100644 identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json create mode 100644 identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=password.json create mode 100644 identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=webauthn.json diff --git a/cipher/chacha20.go b/cipher/chacha20.go index 6ee73ea055f8..46cf1efc85d9 100644 --- a/cipher/chacha20.go +++ b/cipher/chacha20.go @@ -72,7 +72,7 @@ func (c *XChaCha20Poly1305) Decrypt(ctx context.Context, ciphertext string) ([]b for i := range secrets { aead, err := chacha20poly1305.NewX(secrets[i][:]) if err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to instanciate chacha20")) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to instantiate chacha20")) } if len(ciphertext) < aead.NonceSize() { diff --git a/driver/registry_default.go b/driver/registry_default.go index 9317846d81f0..f622d4c20336 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -473,7 +473,7 @@ func (m *RegistryDefault) Cipher(ctx context.Context) cipher.Cipher { m.crypter = cipher.NewCryptAES(m) default: m.crypter = cipher.NewNoop(m) - m.l.Logger.Warning("No encryption configuration found. Default algorithm (noop) will be use that mean sensitive data will be recorded in plaintext") + m.l.Logger.Warning("No encryption configuration found. The default algorithm (noop) will be used, resulting in sensitive data being stored in plaintext") } } return m.crypter diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json new file mode 100644 index 000000000000..a967e155d02a --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json @@ -0,0 +1,22 @@ +{ + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "config": { + "providers": [ + { + "initial_id_token": "foo", + "initial_access_token": "", + "initial_refresh_token": "", + "subject": "", + "provider": "", + "organization": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=password.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=password.json new file mode 100644 index 000000000000..1939a8fe4f71 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=password.json @@ -0,0 +1,10 @@ +{ + "type": "password", + "identifiers": [ + "zab", + "bar" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=webauthn.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=webauthn.json new file mode 100644 index 000000000000..1b7dcd8f6204 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=webauthn.json @@ -0,0 +1,10 @@ +{ + "type": "webauthn", + "identifiers": [ + "foo", + "bar" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/handler_test.go b/identity/handler_test.go index c28d67638266..1de5f6d0864b 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -364,17 +364,19 @@ func TestHandler(t *testing.T) { identities := res.Array() require.Equal(t, len(identities), listAmount) }) - }) t.Run("suite=create and update", func(t *testing.T) { var i identity.Identity createOidcIdentity := func(t *testing.T, identifier, accessToken, refreshToken, idToken string, encrypt bool) string { - transform := func(token string) string { + transform := func(token, suffix string) string { if !encrypt { return token } - c, err := reg.Cipher(ctx).Encrypt(context.Background(), []byte(token)) + if token == "" { + return "" + } + c, err := reg.Cipher(ctx).Encrypt(context.Background(), []byte(token+suffix)) require.NoError(t, err) return c } @@ -396,16 +398,16 @@ func TestHandler(t *testing.T) { { Subject: "foo", Provider: "bar", - InitialAccessToken: transform(accessToken + "0"), - InitialRefreshToken: transform(refreshToken + "0"), - InitialIDToken: transform(idToken + "0"), + InitialAccessToken: transform(accessToken, "0"), + InitialRefreshToken: transform(refreshToken, "0"), + InitialIDToken: transform(idToken, "0"), }, { Subject: "baz", Provider: "zab", - InitialAccessToken: transform(accessToken + "1"), - InitialRefreshToken: transform(refreshToken + "1"), - InitialIDToken: transform(idToken + "1"), + InitialAccessToken: transform(accessToken, "1"), + InitialRefreshToken: transform(refreshToken, "1"), + InitialIDToken: transform(idToken, "1"), }, }}), }, @@ -537,6 +539,34 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=should not fail on empty tokens", func(t *testing.T) { + id := createOidcIdentity(t, "foo.oidc.empty-tokens@bar.com", "", "", "", true) + for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + res := get(t, ts, "/identities/"+id, http.StatusOK) + assert.False(t, res.Get("credentials.oidc.config").Exists(), "credentials config should be omitted: %s", res.Raw) + assert.False(t, res.Get("credentials.password.config").Exists(), "credentials config should be omitted: %s", res.Raw) + + res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) + assert.True(t, res.Get("credentials").Exists(), "credentials should be included: %s", res.Raw) + assert.True(t, res.Get("credentials.password").Exists(), "password meta should be included: %s", res.Raw) + assert.False(t, res.Get("credentials.password.false").Exists(), "password credentials should not be included: %s", res.Raw) + assert.True(t, res.Get("credentials.oidc.config").Exists(), "oidc credentials should be included: %s", res.Raw) + + assert.EqualValues(t, "foo", res.Get("credentials.oidc.config.providers.0.subject").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "bar", res.Get("credentials.oidc.config.providers.0.provider").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "access_token0", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "refresh_token0", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "id_token0", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "baz", res.Get("credentials.oidc.config.providers.1.subject").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "zab", res.Get("credentials.oidc.config.providers.1.provider").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "access_token1", res.Get("credentials.oidc.config.providers.1.initial_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "refresh_token1", res.Get("credentials.oidc.config.providers.1.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "id_token1", res.Get("credentials.oidc.config.providers.1.initial_id_token").String(), "credentials should be included: %s", res.Raw) + }) + } + }) + t.Run("case=should get identity with credentials", func(t *testing.T) { i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) credentials := map[identity.CredentialsType]identity.Credentials{ diff --git a/identity/identity.go b/identity/identity.go index d763e688a8e3..63839a8fad05 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -441,49 +441,48 @@ func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Pro toPublish := original toPublish.Config = []byte{} - for _, token := range []string{"initial_id_token", "initial_access_token", "initial_refresh_token"} { - var i int - var err error - gjson.GetBytes(original.Config, "providers").ForEach(func(_, v gjson.Result) bool { + var i int + var err error + gjson.GetBytes(original.Config, "providers").ForEach(func(_, v gjson.Result) bool { + for _, token := range []string{"initial_id_token", "initial_access_token", "initial_refresh_token"} { key := fmt.Sprintf("%d.%s", i, token) ciphertext := v.Get(token).String() var plaintext []byte - plaintext, err = c.Cipher(ctx).Decrypt(ctx, ciphertext) + plaintext, err := c.Cipher(ctx).Decrypt(ctx, ciphertext) if err != nil { - return false + plaintext = []byte("") } - toPublish.Config, err = sjson.SetBytes(toPublish.Config, "providers."+key, string(plaintext)) if err != nil { return false } + } - toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.subject", i), v.Get("subject").String()) - if err != nil { - return false - } - - toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.provider", i), v.Get("provider").String()) - if err != nil { - return false - } - - toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.organization", i), v.Get("organization").String()) - if err != nil { - return false - } + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.subject", i), v.Get("subject").String()) + if err != nil { + return false + } - i++ - return true - }) + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.provider", i), v.Get("provider").String()) + if err != nil { + return false + } + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.organization", i), v.Get("organization").String()) if err != nil { - return nil, err + return false } - credsToPublish[ct] = toPublish + i++ + return true + }) + + if err != nil { + return nil, err } + + credsToPublish[ct] = toPublish default: credsToPublish[ct] = original } diff --git a/identity/identity_test.go b/identity/identity_test.go index b20ee23e1652..726011fd00eb 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -5,12 +5,14 @@ package identity import ( "bytes" + "context" "encoding/json" "fmt" "testing" "github.com/ory/x/snapshotx" + "github.com/ory/kratos/cipher" "github.com/ory/kratos/x" "github.com/stretchr/testify/require" @@ -314,6 +316,12 @@ func TestVerifiableAddresses(t *testing.T) { assert.Equal(t, addresses, CollectVerifiableAddresses([]*Identity{id1, id2, id3})) } +type cipherProvider struct{} + +func (c *cipherProvider) Cipher(ctx context.Context) cipher.Cipher { + return cipher.NewNoop(nil) +} + func TestWithDeclassifiedCredentials(t *testing.T) { i := NewIdentity(config.DefaultIdentityTraitsSchemaID) credentials := map[CredentialsType]Credentials{ @@ -325,7 +333,7 @@ func TestWithDeclassifiedCredentials(t *testing.T) { CredentialsTypeOIDC: { Type: CredentialsTypeOIDC, Identifiers: []string{"bar", "baz"}, - Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + Config: sqlxx.JSONRawMessage(`{"providers": [{"initial_id_token": "666f6f"}]}`), }, CredentialsTypeWebAuthn: { Type: CredentialsTypeWebAuthn, @@ -336,7 +344,7 @@ func TestWithDeclassifiedCredentials(t *testing.T) { i.Credentials = credentials t.Run("case=no-include", func(t *testing.T) { - actualIdentity, err := i.WithDeclassifiedCredentials(ctx, nil, nil) + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, nil) require.NoError(t, err) for ct, actual := range actualIdentity.Credentials { @@ -347,7 +355,7 @@ func TestWithDeclassifiedCredentials(t *testing.T) { }) t.Run("case=include-webauthn", func(t *testing.T) { - actualIdentity, err := i.WithDeclassifiedCredentials(ctx, nil, []CredentialsType{CredentialsTypeWebAuthn}) + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, []CredentialsType{CredentialsTypeWebAuthn}) require.NoError(t, err) for ct, actual := range actualIdentity.Credentials { @@ -358,7 +366,18 @@ func TestWithDeclassifiedCredentials(t *testing.T) { }) t.Run("case=include-multi", func(t *testing.T) { - actualIdentity, err := i.WithDeclassifiedCredentials(ctx, nil, []CredentialsType{CredentialsTypeWebAuthn, CredentialsTypePassword}) + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, []CredentialsType{CredentialsTypeWebAuthn, CredentialsTypePassword}) + require.NoError(t, err) + + for ct, actual := range actualIdentity.Credentials { + t.Run("credential="+string(ct), func(t *testing.T) { + snapshotx.SnapshotT(t, actual) + }) + } + }) + + t.Run("case=oidc", func(t *testing.T) { + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, []CredentialsType{CredentialsTypeOIDC}) require.NoError(t, err) for ct, actual := range actualIdentity.Credentials { From 7277368bc28df8f0badffc7e739cef20f05e9a02 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:58:03 +0100 Subject: [PATCH 014/262] test: resolve failing test for empty tokens (#3775) --- identity/handler_test.go | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/identity/handler_test.go b/identity/handler_test.go index 1de5f6d0864b..4bf784e033c4 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -555,14 +555,14 @@ func TestHandler(t *testing.T) { assert.EqualValues(t, "foo", res.Get("credentials.oidc.config.providers.0.subject").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "bar", res.Get("credentials.oidc.config.providers.0.provider").String(), "credentials should be included: %s", res.Raw) - assert.EqualValues(t, "access_token0", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "credentials should be included: %s", res.Raw) - assert.EqualValues(t, "refresh_token0", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) - assert.EqualValues(t, "id_token0", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "baz", res.Get("credentials.oidc.config.providers.1.subject").String(), "credentials should be included: %s", res.Raw) assert.EqualValues(t, "zab", res.Get("credentials.oidc.config.providers.1.provider").String(), "credentials should be included: %s", res.Raw) - assert.EqualValues(t, "access_token1", res.Get("credentials.oidc.config.providers.1.initial_access_token").String(), "credentials should be included: %s", res.Raw) - assert.EqualValues(t, "refresh_token1", res.Get("credentials.oidc.config.providers.1.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) - assert.EqualValues(t, "id_token1", res.Get("credentials.oidc.config.providers.1.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_id_token").String(), "credentials should be included: %s", res.Raw) }) } }) @@ -619,8 +619,8 @@ func TestHandler(t *testing.T) { assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) t.Logf("get oidc token") - res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusInternalServerError) - assert.Contains(t, res.Raw, "Internal Server Error", res.Raw) + res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) + assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) }) } @@ -633,8 +633,8 @@ func TestHandler(t *testing.T) { assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) t.Logf("get oidc token") - res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusInternalServerError) - assert.Contains(t, res.Raw, "Internal Server Error", res.Raw) + res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) + assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) }) } }) @@ -1344,15 +1344,11 @@ func TestHandler(t *testing.T) { }) t.Run("case=should list all identities with credentials", func(t *testing.T) { - for name, ts := range map[string]*httptest.Server{"admin": adminTS} { - t.Run("endpoint="+name, func(t *testing.T) { - res := get(t, ts, "/identities?include_credential=totp", http.StatusOK) - assert.True(t, res.Get("0.credentials").Exists(), "credentials config should be included: %s", res.Raw) - assert.True(t, res.Get("0.metadata_public").Exists(), "metadata_public config should be included: %s", res.Raw) - assert.True(t, res.Get("0.metadata_admin").Exists(), "metadata_admin config should be included: %s", res.Raw) - assert.EqualValues(t, "baz", res.Get(`#(traits.bar=="baz").traits.bar`).String(), "%s", res.Raw) - }) - } + res := get(t, adminTS, "/identities?include_credential=totp", http.StatusOK) + assert.True(t, res.Get("0.credentials").Exists(), "credentials config should be included: %s", res.Raw) + assert.True(t, res.Get("0.metadata_public").Exists(), "metadata_public config should be included: %s", res.Raw) + assert.True(t, res.Get("0.metadata_admin").Exists(), "metadata_admin config should be included: %s", res.Raw) + assert.EqualValues(t, "baz", res.Get(`#(traits.bar=="baz").traits.bar`).String(), "%s", res.Raw) }) t.Run("case=should not be able to list all identities with credentials due to wrong credentials type", func(t *testing.T) { From a1bf427e7fa925183989da3b9fecf4149f7f945d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:42:58 +0000 Subject: [PATCH 015/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 299 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 244 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6142899fb09f..dad3f6e381e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ **Table of Contents** -- [ (2024-02-16)](#2024-02-16) +- [ (2024-02-22)](#2024-02-22) - [Bug Fixes](#bug-fixes) - [Features](#features) - [Tests](#tests) -- [1.1.0-pre.0 (2024-02-01)](#110-pre0-2024-02-01) + - [Unclassified](#unclassified) +- [1.1.0 (2024-02-20)](#110-2024-02-20) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes-1) - [Code Generation](#code-generation) @@ -17,14 +18,14 @@ - [Features](#features-1) - [Reverts](#reverts) - [Tests](#tests-1) - - [Unclassified](#unclassified) + - [Unclassified](#unclassified-1) - [1.0.0 (2023-07-12)](#100-2023-07-12) - [Bug Fixes](#bug-fixes-2) - [Code Generation](#code-generation-1) - [Documentation](#documentation-1) - [Features](#features-2) - [Tests](#tests-2) - - [Unclassified](#unclassified-1) + - [Unclassified](#unclassified-2) - [0.13.0 (2023-04-18)](#0130-2023-04-18) - [Breaking Changes](#breaking-changes-1) - [Bug Fixes](#bug-fixes-3) @@ -33,7 +34,7 @@ - [Documentation](#documentation-2) - [Features](#features-3) - [Tests](#tests-3) - - [Unclassified](#unclassified-2) + - [Unclassified](#unclassified-3) - [0.11.1 (2023-01-14)](#0111-2023-01-14) - [Breaking Changes](#breaking-changes-2) - [Bug Fixes](#bug-fixes-4) @@ -53,7 +54,7 @@ - [Features](#features-6) - [Reverts](#reverts-1) - [Tests](#tests-5) - - [Unclassified](#unclassified-3) + - [Unclassified](#unclassified-4) - [0.10.1 (2022-06-01)](#0101-2022-06-01) - [Bug Fixes](#bug-fixes-6) - [Code Generation](#code-generation-6) @@ -65,7 +66,7 @@ - [Documentation](#documentation-5) - [Features](#features-7) - [Tests](#tests-6) - - [Unclassified](#unclassified-4) + - [Unclassified](#unclassified-5) - [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25) - [Breaking Changes](#breaking-changes-5) - [Bug Fixes](#bug-fixes-8) @@ -82,7 +83,7 @@ - [Documentation](#documentation-7) - [Features](#features-8) - [Tests](#tests-7) - - [Unclassified](#unclassified-5) + - [Unclassified](#unclassified-6) - [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21) - [Breaking Changes](#breaking-changes-7) - [Bug Fixes](#bug-fixes-11) @@ -122,7 +123,7 @@ - [Features](#features-12) - [Reverts](#reverts-2) - [Tests](#tests-11) - - [Unclassified](#unclassified-6) + - [Unclassified](#unclassified-7) - [0.7.6-alpha.1 (2021-09-12)](#076-alpha1-2021-09-12) - [Code Generation](#code-generation-18) - [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11) @@ -151,7 +152,7 @@ - [Documentation](#documentation-16) - [Features](#features-15) - [Tests](#tests-14) - - [Unclassified](#unclassified-7) + - [Unclassified](#unclassified-8) - [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17) - [Breaking Changes](#breaking-changes-11) - [Bug Fixes](#bug-fixes-21) @@ -175,14 +176,14 @@ - [Documentation](#documentation-18) - [Features](#features-18) - [Tests](#tests-15) - - [Unclassified](#unclassified-8) + - [Unclassified](#unclassified-9) - [0.5.5-alpha.1 (2020-12-09)](#055-alpha1-2020-12-09) - [Bug Fixes](#bug-fixes-24) - [Code Generation](#code-generation-29) - [Documentation](#documentation-19) - [Features](#features-19) - [Tests](#tests-16) - - [Unclassified](#unclassified-9) + - [Unclassified](#unclassified-10) - [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11) - [Bug Fixes](#bug-fixes-25) - [Code Generation](#code-generation-30) @@ -206,7 +207,7 @@ - [Documentation](#documentation-23) - [Features](#features-22) - [Tests](#tests-19) - - [Unclassified](#unclassified-10) + - [Unclassified](#unclassified-11) - [0.5.0-alpha.1 (2020-10-15)](#050-alpha1-2020-10-15) - [Breaking Changes](#breaking-changes-13) - [Bug Fixes](#bug-fixes-29) @@ -215,7 +216,7 @@ - [Documentation](#documentation-24) - [Features](#features-23) - [Tests](#tests-20) - - [Unclassified](#unclassified-11) + - [Unclassified](#unclassified-12) - [0.4.6-alpha.1 (2020-07-13)](#046-alpha1-2020-07-13) - [Bug Fixes](#bug-fixes-30) - [Code Generation](#code-generation-35) @@ -239,7 +240,7 @@ - [Code Refactoring](#code-refactoring-11) - [Documentation](#documentation-26) - [Features](#features-24) - - [Unclassified](#unclassified-12) + - [Unclassified](#unclassified-13) - [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15) - [Breaking Changes](#breaking-changes-15) - [Bug Fixes](#bug-fixes-36) @@ -247,7 +248,7 @@ - [Code Refactoring](#code-refactoring-12) - [Documentation](#documentation-27) - [Features](#features-25) - - [Unclassified](#unclassified-13) + - [Unclassified](#unclassified-14) - [0.2.1-alpha.1 (2020-05-05)](#021-alpha1-2020-05-05) - [Chores](#chores-1) - [Documentation](#documentation-28) @@ -258,7 +259,7 @@ - [Code Refactoring](#code-refactoring-13) - [Documentation](#documentation-29) - [Features](#features-26) - - [Unclassified](#unclassified-14) + - [Unclassified](#unclassified-15) - [0.1.1-alpha.1 (2020-02-18)](#011-alpha1-2020-02-18) - [Bug Fixes](#bug-fixes-38) - [Code Refactoring](#code-refactoring-14) @@ -280,79 +281,248 @@ - [Bug Fixes](#bug-fixes-40) - [Documentation](#documentation-34) - [Features](#features-29) - - [Unclassified](#unclassified-15) + - [Unclassified](#unclassified-16) - [0.1.0-alpha.1 (2020-01-31)](#010-alpha1-2020-01-31) - [Documentation](#documentation-35) - [0.0.3-alpha.15 (2020-01-31)](#003-alpha15-2020-01-31) - - [Unclassified](#unclassified-16) -- [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31) - [Unclassified](#unclassified-17) -- [0.0.3-alpha.13 (2020-01-31)](#003-alpha13-2020-01-31) +- [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31) - [Unclassified](#unclassified-18) -- [0.0.3-alpha.11 (2020-01-31)](#003-alpha11-2020-01-31) +- [0.0.3-alpha.13 (2020-01-31)](#003-alpha13-2020-01-31) - [Unclassified](#unclassified-19) -- [0.0.3-alpha.10 (2020-01-31)](#003-alpha10-2020-01-31) +- [0.0.3-alpha.11 (2020-01-31)](#003-alpha11-2020-01-31) - [Unclassified](#unclassified-20) -- [0.0.3-alpha.7 (2020-01-30)](#003-alpha7-2020-01-30) +- [0.0.3-alpha.10 (2020-01-31)](#003-alpha10-2020-01-31) - [Unclassified](#unclassified-21) +- [0.0.3-alpha.7 (2020-01-30)](#003-alpha7-2020-01-30) + - [Unclassified](#unclassified-22) - [0.0.3-alpha.5 (2020-01-30)](#003-alpha5-2020-01-30) - [Continuous Integration](#continuous-integration-2) - - [Unclassified](#unclassified-22) -- [0.0.3-alpha.4 (2020-01-30)](#003-alpha4-2020-01-30) - [Unclassified](#unclassified-23) -- [0.0.3-alpha.2 (2020-01-30)](#003-alpha2-2020-01-30) +- [0.0.3-alpha.4 (2020-01-30)](#003-alpha4-2020-01-30) - [Unclassified](#unclassified-24) -- [0.0.3-alpha.1 (2020-01-30)](#003-alpha1-2020-01-30) +- [0.0.3-alpha.2 (2020-01-30)](#003-alpha2-2020-01-30) - [Unclassified](#unclassified-25) +- [0.0.3-alpha.1 (2020-01-30)](#003-alpha1-2020-01-30) + - [Unclassified](#unclassified-26) - [0.0.1-alpha.9 (2020-01-29)](#001-alpha9-2020-01-29) - [Continuous Integration](#continuous-integration-3) - [0.0.2-alpha.1 (2020-01-29)](#002-alpha1-2020-01-29) - - [Unclassified](#unclassified-26) + - [Unclassified](#unclassified-27) - [0.0.1-alpha.6 (2020-01-29)](#001-alpha6-2020-01-29) - [Continuous Integration](#continuous-integration-4) - [0.0.1-alpha.5 (2020-01-29)](#001-alpha5-2020-01-29) - [Continuous Integration](#continuous-integration-5) - - [Unclassified](#unclassified-27) + - [Unclassified](#unclassified-28) - [0.0.1-alpha.3 (2020-01-28)](#001-alpha3-2020-01-28) - [Continuous Integration](#continuous-integration-6) - [Documentation](#documentation-36) - - [Unclassified](#unclassified-28) + - [Unclassified](#unclassified-29) -# [](https://github.com/ory/kratos/compare/v1.1.0-pre.0...v) (2024-02-16) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-02-22) ### Bug Fixes -- Add consistency flag ([#3733](https://github.com/ory/kratos/issues/3733)) - ([fd79950](https://github.com/ory/kratos/commit/fd7995077307cc101550eda5d7724ea1f68fa98a)) -- Don't require code credential for MFA flows - ([#3753](https://github.com/ory/kratos/issues/3753)) - ([40ed809](https://github.com/ory/kratos/commit/40ed809db631149874864f216a106c43ea5df670)) -- Http courier using should use lower case json - ([#3740](https://github.com/ory/kratos/issues/3740)) - ([84149c4](https://github.com/ory/kratos/commit/84149c4b420ea89f0a16a579c017a8e7e1670204)) -- Set iss from userinfo claims if missing - ([#3744](https://github.com/ory/kratos/issues/3744)) - ([241a911](https://github.com/ory/kratos/commit/241a911af74e8ad7353d6e3cab86db20758b86fc)) +- Add login succeeded event to post registration hook + ([#3739](https://github.com/ory/kratos/issues/3739)) + ([b685fa5](https://github.com/ory/kratos/commit/b685fa5477be2ba099fd2420b27b2411fafc7e51)) +- Add missing indexes and remove unused index + ([6d7372e](https://github.com/ory/kratos/commit/6d7372ee3d88ee4fc552b969dd0ff338dcc0544c)) +- Add missing indexes and remove unused index + ([#3756](https://github.com/ory/kratos/issues/3756)) + ([c905f02](https://github.com/ory/kratos/commit/c905f02473c5d77ab309a45f10251b1ba7e88584)) +- Add sms mfa via parameter to spec + ([#3766](https://github.com/ory/kratos/issues/3766)) + ([b291c95](https://github.com/ory/kratos/commit/b291c959c18c72f5edc55607ab23b4592faf8d53)) +- Ignore decrypt errors in WithDeclassifiedCredentials + ([#3731](https://github.com/ory/kratos/issues/3731)) + ([8f5192f](https://github.com/ory/kratos/commit/8f5192fbb74c4b952029a6856284de8d59027770)) +- Prevent SMTP URL leak on unparsable URL + ([#3770](https://github.com/ory/kratos/issues/3770)) + ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) ### Features -- Add request URL to email and SMS templates - ([bf5f8c3](https://github.com/ory/kratos/commit/bf5f8c3cfb2eb523a77239addb8249adf9f8b31d)) -- Improved webhook tracing ([#3746](https://github.com/ory/kratos/issues/3746)) - ([9d7021d](https://github.com/ory/kratos/commit/9d7021d87f47690c2c1f8000e87b425e49bc9496)) -- List by OIDC cred ([#3721](https://github.com/ory/kratos/issues/3721)) - ([bff9c61](https://github.com/ory/kratos/commit/bff9c61b147648ab139e7e86cda4336b5d1cfd39)) +- Add `include_credential` query param to `/admin/identities` list call + ([#3343](https://github.com/ory/kratos/issues/3343)) + ([d94530a](https://github.com/ory/kratos/commit/d94530a716358895b01b65babd77226fab69f494)) +- Add transient payloads to all flows + ([#3738](https://github.com/ory/kratos/issues/3738)) + ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) ### Tests -- Fix hydra tests on master ([#3737](https://github.com/ory/kratos/issues/3737)) - ([12166b4](https://github.com/ory/kratos/commit/12166b4370d607a069f268227752bb7b18a50b57)) +- Resolve failing test for empty tokens + ([#3775](https://github.com/ory/kratos/issues/3775)) + ([7277368](https://github.com/ory/kratos/commit/7277368bc28df8f0badffc7e739cef20f05e9a02)) -# [1.1.0-pre.0](https://github.com/ory/kratos/compare/v1.0.0...v1.1.0-pre.0) (2024-02-01) +### Unclassified + +- Remove unnecessary COPY command from Dockerfile (#3771) + ([087748c](https://github.com/ory/kratos/commit/087748c0651ff0fc93259f7ab6b10668c09f5eba)), + closes [#3771](https://github.com/ory/kratos/issues/3771) + +# [1.1.0](https://github.com/ory/kratos/compare/v1.0.0...v1.1.0) (2024-02-20) + +![Ory Kratos v1.1.0](https://www.ory.sh/images/newsletter/kratos-1.1.0/banner.png) + +Ory Kratos v1.1 is the most complete, most scalable, and most secure open-source +identity server on the planet, and we are thrilled to announce its release! This +release comes with over 270 commits and an incredible amount of new features and +capabilities! + +- **Phone Verification & 2FA with SMS**: Enhance convenient security with phone + verification and two-factor authentication (2FA) via SMS, integrating easily + with SMS gateways like Twilio. This feature not only adds a convenient layer + of security but also offers a straightforward method for user verification, + increasing your trust in user accounts. +- **Translations & Internationalization**: Ory Kratos now supports multiple + languages, making it accessible to a global audience. This improvement + enhances the user experience by providing a localized interface, ensuring + users interact with the system in their preferred language. +- **Native Support for Sign in with Google and Apple on Android/iOS**: Get more + sign-ups with native support for "Sign in with Google" and "Sign in with + Apple" on mobile platforms. Great user experience matters! +- **Account Linking**: Simplify user management with new features that + facilitate account linking. If a user registers with a password and later + signs in with a social account sharing the same email, new screens make + account linking straightforward, enhancing user convenience and reducing + support inquiries. +- **Passwordless "Magic Code"**: Introduce a passwordless login method with + "Magic Code," which sends a one-time code to the user's email for sign-up and + login. This method can also serve as a fallback when users forget their + password or their social login is unavailable, streamlining the login process + and improving user accessibility. +- **Session to JWT Conversion**: Convert an Ory Session Cookie or Ory Session + Token into a JSON Web Token (JWT), providing more flexibility in handling + sessions and integrating with other systems. This feature allows for seamless + authentication and authorization processes across different platforms and + services. + +**Note:** To ensure a seamless upgrade experience with minimal impact, some of +these features are gated behind the `feature_flags` config parameter, allowing +controlled deployment and testing. + +The following features have been shipped exclusively to Ory Network for this +version: + +- **[B2B SSO](https://www.ory.sh/docs/kratos/organizations)** allows your + customers to connect their LDAP / Okta / AD / … to your login. Ory selects the + correct login provider based on the user’s email domain. +- [\*\*Significantly better API performance](https://www.ory.sh/docs/api/eventual-consistency)\*\* + for expensive API operations by specifying the desired consistency + (`strong`, `eventual`). +- **Finding users effortlessly** with our new fuzzy search for credential + identifiers available for + the [Identity List API](https://www.ory.sh/docs/kratos/reference/api#tag/identity/operation/listIdentities). + +- Better reliability when sending out emails across different providers. +- Streamlining the HTTP API and improving related SDK methods. +- Better performance when calling the whoami API endpoint, updating identities, + and listing identities. +- The performance of listing identities has significantly improved with the + introduction of keyset pagination. Page pagination is still available but will + be fully deprecated soon. +- Ability to list multiple identities in a batch call. +- Passkeys and WebAuthn now support multiple origins, useful when working with + subdomains. +- The logout flow now redirects the user back to the `return_to` parameter set + in the API call. +- When updating their settings, the user was sometimes incorrectly asked to + confirm the changes by providing their password. This issue has now been + fixed. +- When signing up with an account that already exists, the user will be shown a + hint helping them sign in to their existing account. +- CORS configuration can now be hot-reloaded. +- The integration with Ory OAuth2 / Ory Hydra has improved for logout, login + session management, verification, and recovery flows. +- A new passwordless method has been added: "Magic code". It sends a one-time + code to the user's email during sign-up and log-in. This method can + additionally be used as a fallback login method when the user forgets their + password. +- Integration with social sign-in has improved, and it is now possible to use + the email verified status from the social sign-in provider. +- Ory Elements and the default Ory Account Experience are now internationalized + with translations. +- It is now possible to convert an Ory Session Cookie or Ory Session Token into + a JSON Web Token. +- Recovery on native apps has improved significantly and no longer requires the + user to switch to a browser for the recovery step. +- Administrators can now find users by their identifiers with fuzzy search - + this feature is still in preview. +- Importing HMAC-hashed passwords is now possible. +- Webhooks can now update identity admin metadata. +- New screens have been added to make account linking possible when a user has + registered with a password and later tries signing in with a social account + sharing the same email. +- Ability to revoke all sessions of a user when they change their password. +- Webhooks are now available for all login, registration, and login methods, + including Passkeys, TOTP, and others. +- The login screen now longer shows “ID” for the primary identifier, but instead + extracts the correct label - for example, “Email” or “Username” from the + Identity Schema. +- Login hints help users with guidance when they are unable to sign in (wrong + social sign-in provider) but have an active account. +- Phone numbers can now be verified via an SMS gateway like Twilio. +- SMS OTP is now a two-factor option. Ory Kratos 1.1 is a major release that + marks a significant milestone in our journey. + +We sincerely hope that you find these new features and improvements in Ory +Kratos 1.1 valuable for your projects. To experience the power of the latest +release, we encourage you to get the latest version of Ory +Kratos [here](https://github.com/ory/kratos) or leverage Ory Kratos +in [Ory Network](https://www.ory.sh/network/) — the easiest, simplest, and most +cost-effective way to run Ory. + +For organizations seeking to upgrade their self-hosted solution, **Ory offers +enterprise support services to ensure a smooth transition**. Our team is ready +to assist you throughout the migration process, ensuring uninterrupted access to +the latest features and improvements. Additionally, we provide +various [support plans](https://www.ory.sh/support/) specifically tailored for +self-hosting organizations. These plans offer comprehensive assistance and +guidance to optimize your Ory deployments and meet your unique requirements. We +extend our heartfelt gratitude to the vibrant and supportive Ory Community. +Without your constant support, feedback, and contributions, reaching this +significant milestone would not have been possible. As we continue on this +journey, your feedback and suggestions are invaluable to us. Together, we are +shaping the future of identity management and authentication in the digital +landscape. + +Contributors to this release in no particular +order: [moose115](https://github.com/ory/kratos/commits?author=moose115), [K3das](https://github.com/ory/kratos/commits?author=K3das), [sidartha](https://github.com/ory/kratos/commits?author=sidartha), [efesler](https://github.com/ory/kratos/commits?author=efesler), [BrandonNoad](https://github.com/ory/kratos/commits?author=BrandonNoad) ,[Saancreed](https://github.com/ory/kratos/commits?author=Saancreed), [jpogorzelski](https://github.com/ory/kratos/commits?author=jpogorzelski), [dreksx](https://github.com/ory/kratos/commits?author=dreksx), [martinloesethjensen](https://github.com/ory/kratos/commits?author=martinloesethjensen), [cpoyatos1](https://github.com/ory/kratos/commits?author=cpoyatos1), [misamu](https://github.com/ory/kratos/commits?author=misamu), [tristankenney](https://github.com/ory/kratos/commits?author=tristankenney), [nxy7](https://github.com/ory/kratos/commits?author=nxy7), [anhnmt](https://github.com/ory/kratos/commits?author=anhnmt) + +Are you passionate about security and want to make a meaningful impact in one of +the biggest open-source communities? Join +the [Ory community](https://slack.ory.sh/) and become a part of the new ID +stack. Together, we are building the next generation of IAM solutions that +empower organizations and individuals to secure their identities effectively. +Want to check out Ory Kratos yourself? Use these commands to get your Ory Kratos +project running on the Ory Network: + +``` +brew install ory/tap/cli + +scoop bucket add ory +scoop install ory + +bash <(curl ) -b . ory +sudo mv ./ory /usr/local/bin/ + +ory auth login + +ory create project --name "My first Kratos project" + +ory open account-experience registration + +ory patch identity-config \ + --replace '/identity/default_schema_id="preset://username"' \ + --replace '/identity/schemas=[{"id":"preset://username","url":"preset://username"}]' \ + --format yaml -autogen: pin v1.1.0-pre.0 release commit +ory open account-experience registration +``` ## Breaking Changes @@ -413,6 +583,8 @@ https://github.com/ory/kratos/pull/3480 - Add caching to Jsonnet snippet during session JWT tokenization ([#3699](https://github.com/ory/kratos/issues/3699)) ([1da8180](https://github.com/ory/kratos/commit/1da818072154baa5c0921134919afde595031e94)) +- Add consistency flag ([#3733](https://github.com/ory/kratos/issues/3733)) + ([fd79950](https://github.com/ory/kratos/commit/fd7995077307cc101550eda5d7724ea1f68fa98a)) - Add max-age to default cors headers ([#3584](https://github.com/ory/kratos/issues/3584)) ([c5b4aaa](https://github.com/ory/kratos/commit/c5b4aaa2df5d010b62a99ccf45850583daad3a66)) @@ -496,6 +668,9 @@ https://github.com/ory/kratos/pull/3480 - Don't list org SSOs in settings ([#3637](https://github.com/ory/kratos/issues/3637)) ([6c7068c](https://github.com/ory/kratos/commit/6c7068cf41df51cde5fe9fc79cca84ec6124d38a)) +- Don't require code credential for MFA flows + ([#3753](https://github.com/ory/kratos/issues/3753)) + ([40ed809](https://github.com/ory/kratos/commit/40ed809db631149874864f216a106c43ea5df670)) - Don't require session for OIDC verification ([#3443](https://github.com/ory/kratos/issues/3443)) ([e08f831](https://github.com/ory/kratos/commit/e08f831c2715e515bf58dc2dbb47fc3576421a5c)) @@ -520,6 +695,9 @@ https://github.com/ory/kratos/pull/3480 - False-positives for requiring re-authentication on update ([#3421](https://github.com/ory/kratos/issues/3421)) ([ce8139f](https://github.com/ory/kratos/commit/ce8139f2325a8317388cbcaaa98f3f83d626657b)) +- Http courier using should use lower case json + ([#3740](https://github.com/ory/kratos/issues/3740)) + ([84149c4](https://github.com/ory/kratos/commit/84149c4b420ea89f0a16a579c017a8e7e1670204)) - Identity list pagination in CLI command and SDK ([#3482](https://github.com/ory/kratos/issues/3482)) ([1e8b1ae](https://github.com/ory/kratos/commit/1e8b1aeb4bf866892788986f62a31255372de999)): @@ -709,6 +887,9 @@ https://github.com/ory/kratos/pull/3480 - Schema test errors ([#3528](https://github.com/ory/kratos/issues/3528)) ([bee0341](https://github.com/ory/kratos/commit/bee0341c5bf5708a2210146fc59f050a1b9df663)) +- Set iss from userinfo claims if missing + ([#3744](https://github.com/ory/kratos/issues/3744)) + ([241a911](https://github.com/ory/kratos/commit/241a911af74e8ad7353d6e3cab86db20758b86fc)) - Specify correct minimum versions in migratest ([18b89ea](https://github.com/ory/kratos/commit/18b89ea588d129fa88379f7b0d7f4fd00ec6023d)) - Tracing context passing in /sessions/whoami @@ -750,8 +931,8 @@ https://github.com/ory/kratos/pull/3480 ### Code Generation -- Pin v1.1.0-pre.0 release commit - ([1c3eeb7](https://github.com/ory/kratos/commit/1c3eeb71d6fc83918ac367d1654361f8fd98a93e)) +- Pin v1.1.0 release commit + ([f47675b](https://github.com/ory/kratos/commit/f47675b82012e0ff74b05b9b7e713b3aa2fdda54)) ### Documentation @@ -797,6 +978,8 @@ https://github.com/ory/kratos/pull/3480 - Add OpenTelemetry span for password hash comparison ([#3383](https://github.com/ory/kratos/issues/3383)) ([e3fcf0c](https://github.com/ory/kratos/commit/e3fcf0c31db9742ed61bcf783e37ee119ed19d42)) +- Add request URL to email and SMS templates + ([bf5f8c3](https://github.com/ory/kratos/commit/bf5f8c3cfb2eb523a77239addb8249adf9f8b31d)) - Add sms verification for phone numbers ([#3649](https://github.com/ory/kratos/issues/3649)) ([e3a3c4f](https://github.com/ory/kratos/commit/e3a3c4fe0d6697f6864283daf4be8a8f8971c7b4)) @@ -989,6 +1172,8 @@ https://github.com/ory/kratos/pull/3480 - Improve performance by computing password hashes while validating ([#3508](https://github.com/ory/kratos/issues/3508)) ([a9786c5](https://github.com/ory/kratos/commit/a9786c599d09f61e2e07df5066ce94feb2d99bac)) +- Improved webhook tracing ([#3746](https://github.com/ory/kratos/issues/3746)) + ([9d7021d](https://github.com/ory/kratos/commit/9d7021d87f47690c2c1f8000e87b425e49bc9496)) - Jsonnet caching for OIDC claims mapper, webhooks, JWT session tokenizer ([#3701](https://github.com/ory/kratos/issues/3701)) ([1d26e09](https://github.com/ory/kratos/commit/1d26e097b273aeda36f73637765da5bdb2aa4a66)) @@ -1005,6 +1190,8 @@ https://github.com/ory/kratos/pull/3480 allows user to link OIDC credentials to existing account right in the login flow, without switching to settings flow. +- List by OIDC cred ([#3721](https://github.com/ory/kratos/issues/3721)) + ([bff9c61](https://github.com/ory/kratos/commit/bff9c61b147648ab139e7e86cda4336b5d1cfd39)) - Login with code on any credential type ([#3549](https://github.com/ory/kratos/issues/3549)) ([ceed7d5](https://github.com/ory/kratos/commit/ceed7d5478c5cca894587698c57f676dda100b27)): @@ -1115,6 +1302,8 @@ https://github.com/ory/kratos/pull/3480 - Fix e2e failures and speed up e2e tests ([#3483](https://github.com/ory/kratos/issues/3483)) ([70a6171](https://github.com/ory/kratos/commit/70a617194d61763f4b75691b22cfa76ba71ab019)) +- Fix hydra tests on master ([#3737](https://github.com/ory/kratos/issues/3737)) + ([12166b4](https://github.com/ory/kratos/commit/12166b4370d607a069f268227752bb7b18a50b57)) - Reduce logging in go tests ([#3562](https://github.com/ory/kratos/issues/3562)) ([05de3a2](https://github.com/ory/kratos/commit/05de3a29fed020593c44ea7a7b29e45197fef4f7)) From 7f8a7f142a91c8c74f32eadb41224fc4f69c2109 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Fri, 23 Feb 2024 09:56:03 +0100 Subject: [PATCH 016/262] fix: test assertions on declassifying OIDC tokens (#3773) Co-authored-by: aeneasr <3372410+aeneasr@users.noreply.github.com> --- identity/handler_test.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/identity/handler_test.go b/identity/handler_test.go index 4bf784e033c4..9444da92a0e8 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -610,22 +610,25 @@ func TestHandler(t *testing.T) { } }) - t.Run("case=should fail to get oidc credential", func(t *testing.T) { + t.Run("case=should return empty tokens if decryption fails", func(t *testing.T) { id := createOidcIdentity(t, "foo-failed.oidc@bar.com", "foo_token", "bar_token", "id_token", false) for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { - t.Logf("no oidc token") res := get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) - t.Logf("get oidc token") res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) - assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) + assert.Equal(t, "bar:foo-failed.oidc@bar.com", res.Get("credentials.oidc.identifiers.0").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "%s", res.Raw) }) } + }) + t.Run("case=should return decrypted token", func(t *testing.T) { e, _ := reg.Cipher(ctx).Encrypt(context.Background(), []byte("foo_token")) - id = createOidcIdentity(t, "foo-failed-2.oidc@bar.com", e, "bar_token", "id_token", false) + id := createOidcIdentity(t, "foo-failed-2.oidc@bar.com", e, "bar_token", "id_token", false) for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { t.Logf("no oidc token") @@ -634,7 +637,8 @@ func TestHandler(t *testing.T) { t.Logf("get oidc token") res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) - assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) + assert.Equal(t, "bar:foo-failed-2.oidc@bar.com", res.Get("credentials.oidc.identifiers.0").String(), "%s", res.Raw) + assert.Equal(t, "foo_token", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "%s", res.Raw) }) } }) From 9710549ea18c36e0f580ea5496a352d4f9d54fec Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 23 Feb 2024 09:42:00 +0000 Subject: [PATCH 017/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dad3f6e381e9..68e3a3f91c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-02-22)](#2024-02-22) +- [ (2024-02-23)](#2024-02-23) - [Bug Fixes](#bug-fixes) - [Features](#features) - [Tests](#tests) @@ -321,7 +321,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-02-22) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-02-23) ### Bug Fixes @@ -342,6 +342,9 @@ - Prevent SMTP URL leak on unparsable URL ([#3770](https://github.com/ory/kratos/issues/3770)) ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) +- Test assertions on declassifying OIDC tokens + ([#3773](https://github.com/ory/kratos/issues/3773)) + ([7f8a7f1](https://github.com/ory/kratos/commit/7f8a7f142a91c8c74f32eadb41224fc4f69c2109)) ### Features From 930fb19842e527e5e9c415efa983b36e02829516 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:55:08 +0100 Subject: [PATCH 018/262] feat: add twitter SSO (#3778) --- embedx/config.schema.json | 3 +- go.mod | 1 + go.sum | 2 + identity/credentials_oidc.go | 44 ++++- identity/credentials_oidc_test.go | 6 +- internal/client-go/go.sum | 1 + selfservice/flow/login/hook_test.go | 4 +- selfservice/strategy/oidc/provider.go | 22 ++- selfservice/strategy/oidc/provider_config.go | 1 + .../strategy/oidc/provider_dingtalk.go | 2 +- .../strategy/oidc/provider_generic_test.go | 4 +- .../strategy/oidc/provider_google_test.go | 4 +- .../strategy/oidc/provider_linkedin_test.go | 4 +- .../oidc/provider_private_net_test.go | 3 +- .../strategy/oidc/provider_userinfo_test.go | 4 +- selfservice/strategy/oidc/provider_x.go | 164 ++++++++++++++++++ selfservice/strategy/oidc/strategy.go | 124 ++++++++++--- selfservice/strategy/oidc/strategy_login.go | 14 +- .../strategy/oidc/strategy_registration.go | 35 +--- .../strategy/oidc/strategy_settings.go | 43 ++--- test/e2e/cypress/downloads/downloads.html | Bin 4680 -> 0 bytes test/e2e/playwright/playwright.env | 23 +++ 22 files changed, 387 insertions(+), 121 deletions(-) create mode 100644 selfservice/strategy/oidc/provider_x.go delete mode 100644 test/e2e/cypress/downloads/downloads.html create mode 100644 test/e2e/playwright/playwright.env diff --git a/embedx/config.schema.json b/embedx/config.schema.json index a836c21a7af5..65eacf9cfbd4 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -436,7 +436,8 @@ "dingtalk", "patreon", "linkedin", - "lark" + "lark", + "x" ], "examples": ["google"] }, diff --git a/go.mod b/go.mod index 22e8187de37e..41effb40c1c8 100644 --- a/go.mod +++ b/go.mod @@ -327,6 +327,7 @@ require ( require ( github.com/coreos/go-oidc/v3 v3.9.0 + github.com/dghubble/oauth1 v0.7.2 github.com/lestrrat-go/jwx/v2 v2.0.19 ) diff --git a/go.sum b/go.sum index d559b4b6ef35..9f68475d07fc 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6/go.mod h1:+ github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/dghubble/oauth1 v0.7.2 h1:pwcinOZy8z6XkNxvPmUDY52M7RDPxt0Xw1zgZ6Cl5JA= +github.com/dghubble/oauth1 v0.7.2/go.mod h1:9erQdIhqhOHG/7K9s/tgh9Ks/AfoyrO5mW/43Lu2+kE= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= diff --git a/identity/credentials_oidc.go b/identity/credentials_oidc.go index 09b5d0aecad0..27462f927024 100644 --- a/identity/credentials_oidc.go +++ b/identity/credentials_oidc.go @@ -32,8 +32,36 @@ type CredentialsOIDCProvider struct { Organization string `json:"organization,omitempty"` } +// swagger:ignore +type CredentialsOIDCEncryptedTokens struct { + RefreshToken string `json:"refresh_token,omitempty"` + IDToken string `json:"id_token,omitempty"` + AccessToken string `json:"access_token,omitempty"` +} + +func (c *CredentialsOIDCEncryptedTokens) GetRefreshToken() string { + if c == nil { + return "" + } + return c.RefreshToken +} + +func (c *CredentialsOIDCEncryptedTokens) GetAccessToken() string { + if c == nil { + return "" + } + return c.AccessToken +} + +func (c *CredentialsOIDCEncryptedTokens) GetIDToken() string { + if c == nil { + return "" + } + return c.IDToken +} + // NewCredentialsOIDC creates a new OIDC credential. -func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, organization string) (*Credentials, error) { +func NewCredentialsOIDC(tokens *CredentialsOIDCEncryptedTokens, provider, subject, organization string) (*Credentials, error) { if provider == "" { return nil, errors.New("received empty provider in oidc credentials") } @@ -48,9 +76,9 @@ func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, o { Subject: subject, Provider: provider, - InitialIDToken: idToken, - InitialAccessToken: accessToken, - InitialRefreshToken: refreshToken, + InitialIDToken: tokens.GetIDToken(), + InitialAccessToken: tokens.GetAccessToken(), + InitialRefreshToken: tokens.GetRefreshToken(), Organization: organization, }}, }); err != nil { @@ -65,6 +93,14 @@ func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, o }, nil } +func (c *CredentialsOIDCProvider) GetTokens() *CredentialsOIDCEncryptedTokens { + return &CredentialsOIDCEncryptedTokens{ + RefreshToken: c.InitialRefreshToken, + IDToken: c.InitialIDToken, + AccessToken: c.InitialAccessToken, + } +} + func OIDCUniqueID(provider, subject string) string { return fmt.Sprintf("%s:%s", provider, subject) } diff --git a/identity/credentials_oidc_test.go b/identity/credentials_oidc_test.go index dda20f73deb7..f52a077d107c 100644 --- a/identity/credentials_oidc_test.go +++ b/identity/credentials_oidc_test.go @@ -10,10 +10,10 @@ import ( ) func TestNewCredentialsOIDC(t *testing.T) { - _, err := NewCredentialsOIDC("", "", "", "", "not-empty", "") + _, err := NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "", "not-empty", "") require.Error(t, err) - _, err = NewCredentialsOIDC("", "", "", "not-empty", "", "") + _, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "", "") require.Error(t, err) - _, err = NewCredentialsOIDC("", "", "", "not-empty", "not-empty", "") + _, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "not-empty", "") require.NoError(t, err) } diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index 46f957c1ca76..fe73f22d7eef 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -300,9 +300,7 @@ func TestLoginExecutor(t *testing.T) { require.NoError(t, reg.Persister().CreateIdentity(context.Background(), useIdentity)) credsOIDC, err := identity.NewCredentialsOIDC( - "id-token", - "access-token", - "refresh-token", + &identity.CredentialsOIDCEncryptedTokens{IDToken: "id-token", AccessToken: "access-token", RefreshToken: "refresh-token"}, "my-provider", email, "", diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go index 4fe9a028c11e..cb22ebb6b847 100644 --- a/selfservice/strategy/oidc/provider.go +++ b/selfservice/strategy/oidc/provider.go @@ -5,8 +5,10 @@ package oidc import ( "context" + "net/http" "net/url" + "github.com/dghubble/oauth1" "github.com/pkg/errors" "github.com/ory/herodot" @@ -18,12 +20,24 @@ import ( type Provider interface { Config() *Configuration +} + +type OAuth2Provider interface { + Provider + AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption OAuth2(ctx context.Context) (*oauth2.Config, error) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) - AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption } -type TokenExchanger interface { +type OAuth1Provider interface { + Provider + OAuth1(ctx context.Context) *oauth1.Config + AuthURL(ctx context.Context, state string) (string, error) + Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) + ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error) +} + +type OAuth2TokenExchanger interface { Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) } @@ -87,11 +101,11 @@ func (c *Claims) Validate() error { // - `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. // - `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. // - `auth_type` (string): The `auth_type` parameter specifies the requested authentication features (as a comma-separated list), e.g. `reauthenticate`. -func UpstreamParameters(provider Provider, upstreamParameters map[string]string) []oauth2.AuthCodeOption { +func UpstreamParameters(upstreamParameters map[string]string) []oauth2.AuthCodeOption { // validation of upstream parameters are already handled in the `oidc/.schema/link.schema.json` and `oidc/.schema/settings.schema.json` file. // `upstreamParameters` will always only contain allowed parameters based on the configuration. - // we double check the parameters here to prevent any potential security issues. + // we double-check the parameters here to prevent any potential security issues. allowedParameters := map[string]struct{}{ "login_hint": {}, "hd": {}, diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index 512a5ecc6fa2..ea4d4364691e 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -160,6 +160,7 @@ var supportedProviders = map[string]func(config *Configuration, reg Dependencies "linkedin": NewProviderLinkedIn, "patreon": NewProviderPatreon, "lark": NewProviderLark, + "x": NewProviderX, } func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) { diff --git a/selfservice/strategy/oidc/provider_dingtalk.go b/selfservice/strategy/oidc/provider_dingtalk.go index 36469e6e7c23..12abffe85942 100644 --- a/selfservice/strategy/oidc/provider_dingtalk.go +++ b/selfservice/strategy/oidc/provider_dingtalk.go @@ -65,7 +65,7 @@ func (g *ProviderDingTalk) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2(ctx), nil } -func (g *ProviderDingTalk) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { +func (g *ProviderDingTalk) ExchangeOAuth2Token(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { conf, err := g.OAuth2(ctx) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) diff --git a/selfservice/strategy/oidc/provider_generic_test.go b/selfservice/strategy/oidc/provider_generic_test.go index 221ca7b1827a..7c90da7e3ec5 100644 --- a/selfservice/strategy/oidc/provider_generic_test.go +++ b/selfservice/strategy/oidc/provider_generic_test.go @@ -45,9 +45,9 @@ func makeAuthCodeURL(t *testing.T, r *login.Flow, reg *driver.RegistryDefault) s Mapper: "file://./stub/hydra.schema.json", RequestedClaims: makeOIDCClaims(), }, reg) - c, err := p.OAuth2(context.Background()) + c, err := p.(oidc.OAuth2Provider).OAuth2(context.Background()) require.NoError(t, err) - return c.AuthCodeURL("state", p.AuthCodeURLOptions(r)...) + return c.AuthCodeURL("state", p.(oidc.OAuth2Provider).AuthCodeURLOptions(r)...) } func TestProviderGenericOIDC_AddAuthCodeURLOptions(t *testing.T) { diff --git a/selfservice/strategy/oidc/provider_google_test.go b/selfservice/strategy/oidc/provider_google_test.go index d900b088d358..c1a1f65b9348 100644 --- a/selfservice/strategy/oidc/provider_google_test.go +++ b/selfservice/strategy/oidc/provider_google_test.go @@ -34,7 +34,7 @@ func TestProviderGoogle_Scope(t *testing.T) { Scope: []string{"email", "profile", "offline_access"}, }, reg) - c, _ := p.OAuth2(context.Background()) + c, _ := p.(oidc.OAuth2Provider).OAuth2(context.Background()) assert.NotContains(t, c.Scopes, "offline_access") } @@ -55,7 +55,7 @@ func TestProviderGoogle_AccessType(t *testing.T) { ID: x.NewUUID(), } - options := p.AuthCodeURLOptions(r) + options := p.(oidc.OAuth2Provider).AuthCodeURLOptions(r) assert.Contains(t, options, oauth2.AccessTypeOffline) } diff --git a/selfservice/strategy/oidc/provider_linkedin_test.go b/selfservice/strategy/oidc/provider_linkedin_test.go index 5eb7a629c7cc..d5b9df86d25a 100644 --- a/selfservice/strategy/oidc/provider_linkedin_test.go +++ b/selfservice/strategy/oidc/provider_linkedin_test.go @@ -115,7 +115,7 @@ func TestProviderLinkedin_Claims(t *testing.T) { linkedin := oidc.NewProviderLinkedIn(c, reg) const fakeLinkedinIDToken = "id_token_mock_" - actual, err := linkedin.Claims( + actual, err := linkedin.(oidc.OAuth2Provider).Claims( context.Background(), (&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}), url.Values{}, @@ -191,7 +191,7 @@ func TestProviderLinkedin_No_Picture(t *testing.T) { linkedin := oidc.NewProviderLinkedIn(c, reg) const fakeLinkedinIDToken = "id_token_mock_" - actual, err := linkedin.Claims( + actual, err := linkedin.(oidc.OAuth2Provider).Claims( context.Background(), (&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}), url.Values{}, diff --git a/selfservice/strategy/oidc/provider_private_net_test.go b/selfservice/strategy/oidc/provider_private_net_test.go index 878b8622e993..e656ee0462bb 100644 --- a/selfservice/strategy/oidc/provider_private_net_test.go +++ b/selfservice/strategy/oidc/provider_private_net_test.go @@ -84,10 +84,11 @@ func TestProviderPrivateIP(t *testing.T) { // VK uses a fixed token URL and does not use the issuer. // Yandex uses a fixed token URL and does not use the issuer. // NetID uses a fixed token URL and does not use the issuer. + // X uses a fixed token URL and userinfoRL and does not use the issuer value. } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { p := tc.p(tc.c) - _, err := p.Claims(context.Background(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{ + _, err := p.(oidc.OAuth2Provider).Claims(context.Background(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{ "id_token": tc.id, }), url.Values{}) require.Error(t, err) diff --git a/selfservice/strategy/oidc/provider_userinfo_test.go b/selfservice/strategy/oidc/provider_userinfo_test.go index 0b11f2dcae90..97456dfc404d 100644 --- a/selfservice/strategy/oidc/provider_userinfo_test.go +++ b/selfservice/strategy/oidc/provider_userinfo_test.go @@ -343,7 +343,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { return resp, err }) - _, err := tc.provider.Claims(ctx, token, url.Values{}) + _, err := tc.provider.(oidc.OAuth2Provider).Claims(ctx, token, url.Values{}) var he *herodot.DefaultError require.ErrorAs(t, err, &he) assert.Equal(t, "OpenID Connect provider returned a 455 status code but 200 is expected.", he.Reason()) @@ -359,7 +359,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { httpmock.RegisterResponder("GET", tc.userInfoEndpoint, tc.userInfoHandler) - claims, err := tc.provider.Claims(ctx, token, url.Values{}) + claims, err := tc.provider.(oidc.OAuth2Provider).Claims(ctx, token, url.Values{}) require.NoError(t, err) if tc.expectedClaims == nil { assert.Equal(t, expectedClaims, claims) diff --git a/selfservice/strategy/oidc/provider_x.go b/selfservice/strategy/oidc/provider_x.go new file mode 100644 index 000000000000..060ba58a6303 --- /dev/null +++ b/selfservice/strategy/oidc/provider_x.go @@ -0,0 +1,164 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/dghubble/oauth1" + "github.com/dghubble/oauth1/twitter" + "github.com/pkg/errors" + + "github.com/ory/herodot" +) + +var _ Provider = (*ProviderX)(nil) +var _ OAuth1Provider = (*ProviderX)(nil) + +const xUserInfoBase = "https://api.twitter.com/1.1/account/verify_credentials.json" +const xUserInfoWithEmail = xUserInfoBase + "?include_email=true" + +type ProviderX struct { + config *Configuration + reg Dependencies +} + +func (p *ProviderX) Config() *Configuration { + return p.config +} + +func NewProviderX( + config *Configuration, + reg Dependencies) Provider { + return &ProviderX{ + config: config, + reg: reg, + } +} + +func (p *ProviderX) ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error) { + requestToken, verifier, err := oauth1.ParseAuthorizationCallback(req) + if err != nil { + return nil, err + } + + accessToken, accessSecret, err := p.OAuth1(ctx).AccessToken(requestToken, "", verifier) + if err != nil { + return nil, err + } + + return oauth1.NewToken(accessToken, accessSecret), nil +} + +func (p *ProviderX) AuthURL(ctx context.Context, state string) (string, error) { + c := p.OAuth1(ctx) + + // We need to cheat so that callback validates on return + c.CallbackURL = c.CallbackURL + fmt.Sprintf("?state=%s&code=unused", state) + + requestToken, _, err := c.RequestToken() + if err != nil { + return "", errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`Unable to sign in with X because the OAuth1 request token could not be initialized.`)) + } + + authzURL, err := c.AuthorizationURL(requestToken) + if err != nil { + return "", errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`Unable to sign in with X because the OAuth1 authorization URL could not be parsed.`)) + } + + return authzURL.String(), nil +} + +func (p *ProviderX) CheckError(ctx context.Context, r *http.Request) error { + if r.URL.Query().Get("denied") == "" { + return nil + } + + return errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to sign in with X because the user denied the request.`)) +} + +func (p *ProviderX) OAuth1(ctx context.Context) *oauth1.Config { + return &oauth1.Config{ + ConsumerKey: p.config.ClientID, + ConsumerSecret: p.config.ClientSecret, + Endpoint: twitter.AuthorizeEndpoint, + CallbackURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (p *ProviderX) userInfoEndpoint() string { + for _, scope := range p.config.Scope { + if scope == "email" { + return xUserInfoWithEmail + } + } + + return xUserInfoBase +} + +func (p *ProviderX) Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) { + ctx = context.WithValue(ctx, oauth1.HTTPClient, p.reg.HTTPClient(ctx).HTTPClient) + + c := p.OAuth1(ctx) + client := c.Client(ctx, token) + endpoint := p.userInfoEndpoint() + + resp, err := client.Get(endpoint) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + defer resp.Body.Close() + + if err := logUpstreamError(p.reg.Logger(), resp); err != nil { + return nil, err + } + + user := &xUser{} + if err := json.NewDecoder(resp.Body).Decode(user); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + website := "" + if user.URL != nil { + website = *user.URL + } + + return &Claims{ + Issuer: endpoint, + Subject: user.IDStr, + Name: user.Name, + Picture: user.ProfileImageURLHTTPS, + Email: user.Email, + PreferredUsername: user.ScreenName, + Website: website, + }, nil +} + +type xUser struct { + ID int `json:"id"` + IDStr string `json:"id_str"` + Name string `json:"name"` + ScreenName string `json:"screen_name"` + Location string `json:"location"` + Description string `json:"description"` + URL *string `json:"url,omitempty"` + Protected bool `json:"protected"` + FollowersCount int `json:"followers_count"` + FriendsCount int `json:"friends_count"` + ListedCount int `json:"listed_count"` + CreatedAt string `json:"created_at"` + FavouritesCount int `json:"favourites_count"` + Verified bool `json:"verified"` + StatusesCount int `json:"statuses_count"` + DefaultProfile bool `json:"default_profile"` + DefaultProfileImage bool `json:"default_profile_image"` + ProfileImageURLHTTPS string `json:"profile_image_url_https"` + WithheldInCountries []string `json:"withheld_in_countries"` + Suspended bool `json:"suspended"` + NeedsPhoneVerification bool `json:"needs_phone_verification"` + Email string `json:"email"` +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 4f5848e8a431..5289b2a9b96b 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -412,16 +412,39 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt return } - token, err := s.ExchangeCode(r.Context(), provider, code) - if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) - return - } + var claims *Claims + var et *identity.CredentialsOIDCEncryptedTokens + switch p := provider.(type) { + case OAuth2Provider: + token, err := s.ExchangeCode(r.Context(), provider, code) + if err != nil { + s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + return + } - claims, err := provider.Claims(r.Context(), token, r.URL.Query()) - if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) - return + et, err = s.encryptOAuth2Tokens(r.Context(), token) + if err != nil { + s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + return + } + + claims, err = p.Claims(r.Context(), token, r.URL.Query()) + if err != nil { + s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + return + } + case OAuth1Provider: + token, err := p.ExchangeToken(r.Context(), r) + if err != nil { + s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + return + } + + claims, err = p.Claims(r.Context(), token) + if err != nil { + s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + return + } } if err := claims.Validate(); err != nil { @@ -431,7 +454,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt switch a := req.(type) { case *login.Flow: - if ff, err := s.processLogin(w, r, a, token, claims, provider, cntnr); err != nil { + if ff, err := s.processLogin(w, r, a, et, claims, provider, cntnr); err != nil { if ff != nil { s.forwardError(w, r, ff, err) return @@ -441,7 +464,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt return case *registration.Flow: a.TransientPayload = cntnr.TransientPayload - if ff, err := s.processRegistration(w, r, a, token, claims, provider, cntnr, ""); err != nil { + if ff, err := s.processRegistration(w, r, a, et, claims, provider, cntnr, ""); err != nil { if ff != nil { s.forwardError(w, r, ff, err) return @@ -455,7 +478,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt s.forwardError(w, r, a, s.handleError(w, r, a, pid, nil, err)) return } - if err := s.linkProvider(w, r, &settings.UpdateContext{Session: sess, Flow: a}, token, claims, provider); err != nil { + if err := s.linkProvider(w, r, &settings.UpdateContext{Session: sess, Flow: a}, et, claims, provider); err != nil { s.forwardError(w, r, a, s.handleError(w, r, a, pid, nil, err)) return } @@ -473,18 +496,23 @@ func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code str span.SetAttributes(attribute.String("provider_id", provider.Config().ID)) span.SetAttributes(attribute.String("provider_label", provider.Config().Label)) - te, ok := provider.(TokenExchanger) - if !ok { - te, err = provider.OAuth2(ctx) - if err != nil { - return nil, err + switch p := provider.(type) { + case OAuth2Provider: + te, ok := provider.(OAuth2TokenExchanger) + if !ok { + te, err = p.OAuth2(ctx) + if err != nil { + return nil, err + } } - } - client := s.d.HTTPClient(ctx) - ctx = context.WithValue(ctx, oauth2.HTTPClient, client.HTTPClient) - token, err = te.Exchange(ctx, code) - return token, err + client := s.d.HTTPClient(ctx) + ctx = context.WithValue(ctx, oauth2.HTTPClient, client.HTTPClient) + token, err = te.Exchange(ctx, code) + return token, err + default: + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The chosen provider is not capable of exchanging an OAuth 2.0 code for an access token.")) + } } func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(provider string) *text.Message) error { @@ -706,7 +734,7 @@ func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provid return claims, nil } -func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, idToken, accessToken, refreshToken, provider, subject, organization string) error { +func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, tokens *identity.CredentialsOIDCEncryptedTokens, provider, subject, organization string) error { if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials); err != nil { return err } @@ -714,7 +742,7 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, id creds, err := i.ParseCredentials(s.ID(), &conf) if errors.Is(err, herodot.ErrNotFound) { var err error - if creds, err = identity.NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, organization); err != nil { + if creds, err = identity.NewCredentialsOIDC(tokens, provider, subject, organization); err != nil { return err } } else if err != nil { @@ -723,9 +751,9 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, id creds.Identifiers = append(creds.Identifiers, identity.OIDCUniqueID(provider, subject)) conf.Providers = append(conf.Providers, identity.CredentialsOIDCProvider{ Subject: subject, Provider: provider, - InitialAccessToken: accessToken, - InitialRefreshToken: refreshToken, - InitialIDToken: idToken, + InitialAccessToken: tokens.GetAccessToken(), + InitialRefreshToken: tokens.GetRefreshToken(), + InitialIDToken: tokens.GetIDToken(), Organization: organization, }) @@ -742,3 +770,45 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, id return nil } + +func getAuthRedirectURL(ctx context.Context, provider Provider, req ider, state *State, upstreamParameters map[string]string) (codeURL string, err error) { + switch p := provider.(type) { + case OAuth2Provider: + c, err := p.OAuth2(ctx) + if err != nil { + return "", err + } + + return c.AuthCodeURL(state.String(), append(UpstreamParameters(upstreamParameters), p.AuthCodeURLOptions(req)...)...), nil + case OAuth1Provider: + return p.AuthURL(ctx, state.String()) + default: + return "", errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The provider %s does not support the OAuth 2.0 or OAuth 1.0 protocol", provider.Config().Provider)) + } +} + +func (s *Strategy) encryptOAuth2Tokens(ctx context.Context, token *oauth2.Token) (et *identity.CredentialsOIDCEncryptedTokens, err error) { + et = new(identity.CredentialsOIDCEncryptedTokens) + if token == nil { + return et, nil + } + + if idToken, ok := token.Extra("id_token").(string); ok { + et.IDToken, err = s.d.Cipher(ctx).Encrypt(ctx, []byte(idToken)) + if err != nil { + return nil, err + } + } + + et.AccessToken, err = s.d.Cipher(ctx).Encrypt(ctx, []byte(token.AccessToken)) + if err != nil { + return nil, err + } + + et.RefreshToken, err = s.d.Cipher(ctx).Encrypt(ctx, []byte(token.RefreshToken)) + if err != nil { + return nil, err + } + + return et, nil +} diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index fe90eab8ad4e..09bb8ab27e6e 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -11,7 +11,6 @@ import ( "time" "github.com/julienschmidt/httprouter" - "golang.org/x/oauth2" "github.com/ory/kratos/session" @@ -107,7 +106,7 @@ type UpdateLoginFlowWithOidcMethod struct { TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *AuthCodeContainer) (*registration.Flow, error) { +func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (*registration.Flow, error) { i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) if err != nil { if errors.Is(err, sqlcon.ErrNoRows) { @@ -227,11 +226,6 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleError(w, r, f, pid, nil, err) } - c, err := provider.OAuth2(ctx) - if err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) - } - req, err := s.validateFlow(ctx, r, f.ID) if err != nil { return nil, s.handleError(w, r, f, pid, nil, err) @@ -282,7 +276,11 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - codeURL := c.AuthCodeURL(state.String(), append(UpstreamParameters(provider, up), provider.AuthCodeURLOptions(req)...)...) + codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up) + if err != nil { + return nil, s.handleError(w, r, f, pid, nil, err) + } + if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) } else { diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index f16ba63f07b4..124e8539f6ea 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -16,7 +16,6 @@ import ( "github.com/pkg/errors" "github.com/tidwall/gjson" "github.com/tidwall/sjson" - "golang.org/x/oauth2" "github.com/ory/herodot" "github.com/ory/kratos/continuity" @@ -185,11 +184,6 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return s.handleError(w, r, f, pid, nil, err) } - c, err := provider.OAuth2(ctx) - if err != nil { - return s.handleError(w, r, f, pid, nil, err) - } - req, err := s.validateFlow(ctx, r, f.ID) if err != nil { return s.handleError(w, r, f, pid, nil, err) @@ -237,7 +231,10 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return err } - codeURL := c.AuthCodeURL(state.String(), append(UpstreamParameters(provider, up), provider.AuthCodeURLOptions(req)...)...) + codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up) + if err != nil { + return s.handleError(w, r, f, pid, nil, err) + } if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) } else { @@ -279,7 +276,7 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r return lf, nil } -func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *AuthCodeContainer, idToken string) (*login.Flow, error) { +func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer, idToken string) (*login.Flow, error) { if _, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)); err == nil { // If the identity already exists, we should perform the login flow instead. @@ -334,27 +331,7 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r } } - var it string = idToken - var cat, crt string - if token != nil { - if idToken, ok := token.Extra("id_token").(string); ok { - if it, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(idToken)); err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) - } - } - - cat, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.AccessToken)) - if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) - } - - crt, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.RefreshToken)) - if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) - } - } - - creds, err := identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject, provider.Config().OrganizationID) + creds, err := identity.NewCredentialsOIDC(token, provider.Config().ID, claims.Subject, provider.Config().OrganizationID) if err != nil { return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) } diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index ed94972b1500..4fde3a457548 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -15,8 +15,6 @@ import ( "github.com/tidwall/sjson" - "golang.org/x/oauth2" - "github.com/ory/kratos/continuity" "github.com/ory/kratos/selfservice/strategy" "github.com/ory/x/decoderx" @@ -367,20 +365,15 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU return s.handleSettingsError(w, r, ctxUpdate, p, err) } - c, err := provider.OAuth2(r.Context()) - if err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } - req, err := s.validateFlow(r.Context(), r, ctxUpdate.Flow.ID) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - state := generateState(ctxUpdate.Flow.ID.String()).String() + state := generateState(ctxUpdate.Flow.ID.String()) if err := s.d.ContinuityManager().Pause(r.Context(), w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ - State: state, + State: state.String(), FlowID: ctxUpdate.Flow.ID.String(), Traits: p.Traits, }), @@ -393,7 +386,11 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU return err } - codeURL := c.AuthCodeURL(state, append(UpstreamParameters(provider, up), provider.AuthCodeURLOptions(req)...)...) + codeURL, err := getAuthRedirectURL(r.Context(), provider, req, state, up) + if err != nil { + return s.handleSettingsError(w, r, ctxUpdate, p, err) + } + if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) } else { @@ -403,7 +400,7 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *oauth2.Token, claims *Claims, provider Provider) error { +func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider) error { p := &updateSettingsFlowWithOidcMethod{ Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String(), } @@ -416,24 +413,7 @@ func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdat return s.handleSettingsError(w, r, ctxUpdate, p, err) } - var it string - if idToken, ok := token.Extra("id_token").(string); ok { - if it, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(idToken)); err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } - } - - cat, err := s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.AccessToken)) - if err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } - - crt, err := s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.RefreshToken)) - if err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } - - if err := s.linkCredentials(r.Context(), i, it, cat, crt, provider.Config().ID, claims.Subject, provider.Config().OrganizationID); err != nil { + if err := s.linkCredentials(r.Context(), i, token, provider.Config().ID, claims.Subject, provider.Config().OrganizationID); err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } @@ -546,9 +526,8 @@ func (s *Strategy) Link(ctx context.Context, i *identity.Identity, credentialsCo if err := s.linkCredentials( ctx, i, - credentialsOIDCProvider.InitialIDToken, - credentialsOIDCProvider.InitialAccessToken, - credentialsOIDCProvider.InitialRefreshToken, + // The tokens in this credential are coming from the existing identity. Hence, the values are already encrypted. + credentialsOIDCProvider.GetTokens(), credentialsOIDCProvider.Provider, credentialsOIDCProvider.Subject, credentialsOIDCProvider.Organization, diff --git a/test/e2e/cypress/downloads/downloads.html b/test/e2e/cypress/downloads/downloads.html deleted file mode 100644 index 4e6e883a5f39548b6afb7fccbc6b9fda25d65ef0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4680 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 By#oLM diff --git a/test/e2e/playwright/playwright.env b/test/e2e/playwright/playwright.env new file mode 100644 index 000000000000..8289c433e8a9 --- /dev/null +++ b/test/e2e/playwright/playwright.env @@ -0,0 +1,23 @@ +KRATOS_BROWSER_URL=http://localhost:4433/ +KRATOS_UI_URL=http://localhost:4456/ +KRATOS_UI_REACT_URL=http://localhost:4458/ +KRATOS_UI_REACT_NATIVE_URL=http://localhost:19006/ +KRATOS_ADMIN_URL=http://localhost:4434/ +KRATOS_PUBLIC_URL=http://localhost:4433/ +TEST_DATABASE_MYSQL=mysql://root:secret@(localhost:3444)/mysql?parseTime=true&multiStatements=true +TEST_DATABASE_COCKROACHDB=cockroach://root@localhost:3446/defaultdb?sslmode=disable +TEST_DATABASE_MEMORY=memory +TEST_DATABASE_POSTGRESQL=postgres://postgres:secret@localhost:3445/postgres?sslmode=disable +TEST_DATABASE_SQLITE=sqlite:////var/folders/7v/lfnm0tm91wb6_ngvr0xk5l0h0000gn/T/ci-XXXXXXXXXX.5Bt7rvQC9L/db.sqlite?_fk=true +OIDC_GITHUB_CLIENT_SECRET=cwV-UvqowlDGrxWvU41DvxbsUy +OIDC_HYDRA_CLIENT_ID=d43ab7f7-90a2-4809-876f-8ed6f897d423 +OIDC_GITHUB_CLIENT_ID=05ef7c9a-814c-4cb9-899b-2010328f670a +OIDC_GOOGLE_CLIENT_ID=dc3915c1-0d0a-4376-b4d7-18c45e339882 +CYPRESS_OIDC_DUMMY_CLIENT_ID=75da61f8-e91b-43cb-8b3e-c9f4925194f9 +OIDC_GOOGLE_CLIENT_SECRET=.yT5lLhbOIHiStbOr5xOa-0X1k +OIDC_HYDRA_CLIENT_SECRET=Y9PZnoQ~1lcEo_WCc4.XjeytjG +CYPRESS_OIDC_DUMMY_CLIENT_SECRET=F-H.e6z5_BM9GVIW5y~MqoAw6g +CYPRESS_OIDC_DUMMY_CLIENT_ID=75da61f8-e91b-43cb-8b3e-c9f4925194f9 +CYPRESS_OIDC_DUMMY_CLIENT_SECRET=F-H.e6z5_BM9GVIW5y~MqoAw6g +LOG_LEAK_SENSITIVE_VALUES=true +DEV_DISABLE_API_FLOW_ENFORCEMENT=true From 63ce4707147478857502609d17a257c47107dab4 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:14:00 +0100 Subject: [PATCH 019/262] chore: remove e2e playwright env (#3794) --- test/e2e/playwright/playwright.env | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 test/e2e/playwright/playwright.env diff --git a/test/e2e/playwright/playwright.env b/test/e2e/playwright/playwright.env deleted file mode 100644 index 8289c433e8a9..000000000000 --- a/test/e2e/playwright/playwright.env +++ /dev/null @@ -1,23 +0,0 @@ -KRATOS_BROWSER_URL=http://localhost:4433/ -KRATOS_UI_URL=http://localhost:4456/ -KRATOS_UI_REACT_URL=http://localhost:4458/ -KRATOS_UI_REACT_NATIVE_URL=http://localhost:19006/ -KRATOS_ADMIN_URL=http://localhost:4434/ -KRATOS_PUBLIC_URL=http://localhost:4433/ -TEST_DATABASE_MYSQL=mysql://root:secret@(localhost:3444)/mysql?parseTime=true&multiStatements=true -TEST_DATABASE_COCKROACHDB=cockroach://root@localhost:3446/defaultdb?sslmode=disable -TEST_DATABASE_MEMORY=memory -TEST_DATABASE_POSTGRESQL=postgres://postgres:secret@localhost:3445/postgres?sslmode=disable -TEST_DATABASE_SQLITE=sqlite:////var/folders/7v/lfnm0tm91wb6_ngvr0xk5l0h0000gn/T/ci-XXXXXXXXXX.5Bt7rvQC9L/db.sqlite?_fk=true -OIDC_GITHUB_CLIENT_SECRET=cwV-UvqowlDGrxWvU41DvxbsUy -OIDC_HYDRA_CLIENT_ID=d43ab7f7-90a2-4809-876f-8ed6f897d423 -OIDC_GITHUB_CLIENT_ID=05ef7c9a-814c-4cb9-899b-2010328f670a -OIDC_GOOGLE_CLIENT_ID=dc3915c1-0d0a-4376-b4d7-18c45e339882 -CYPRESS_OIDC_DUMMY_CLIENT_ID=75da61f8-e91b-43cb-8b3e-c9f4925194f9 -OIDC_GOOGLE_CLIENT_SECRET=.yT5lLhbOIHiStbOr5xOa-0X1k -OIDC_HYDRA_CLIENT_SECRET=Y9PZnoQ~1lcEo_WCc4.XjeytjG -CYPRESS_OIDC_DUMMY_CLIENT_SECRET=F-H.e6z5_BM9GVIW5y~MqoAw6g -CYPRESS_OIDC_DUMMY_CLIENT_ID=75da61f8-e91b-43cb-8b3e-c9f4925194f9 -CYPRESS_OIDC_DUMMY_CLIENT_SECRET=F-H.e6z5_BM9GVIW5y~MqoAw6g -LOG_LEAK_SENSITIVE_VALUES=true -DEV_DISABLE_API_FLOW_ENFORCEMENT=true From dee584498a0a5da13f852beb61c93c1f0325f1b9 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:34:02 +0000 Subject: [PATCH 020/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 7c68c5aa69ed76a84a37a37a3555277ddc772cf8 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 4 Mar 2024 10:37:47 +0100 Subject: [PATCH 021/262] fix: make sure emails can still be sent with SMS enabled (#3795) --- driver/config/config.go | 6 +++--- driver/config/config_test.go | 4 +++- driver/config/stub/.kratos.courier.channels.yaml | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index 6af7e7c17c5c..ad2d414fdfa3 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -1020,7 +1020,7 @@ func (p *Config) SelfServiceFlowLogoutRedirectURL(ctx context.Context) *url.URL } func (p *Config) CourierEmailStrategy(ctx context.Context) string { - return p.GetProvider(ctx).String(ViperKeyCourierDeliveryStrategy) + return p.GetProvider(ctx).StringF(ViperKeyCourierDeliveryStrategy, "smtp") } func (p *Config) CourierEmailRequestConfig(ctx context.Context) json.RawMessage { @@ -1169,7 +1169,6 @@ func (p *Config) CourierChannels(ctx context.Context) (ccs []*CourierChannel, _ } } } - return ccs, nil } // load legacy configs @@ -1188,7 +1187,8 @@ func (p *Config) CourierChannels(ctx context.Context) (ccs []*CourierChannel, _ return nil, errors.WithStack(err) } } - return []*CourierChannel{&channel}, nil + ccs = append(ccs, &channel) + return ccs, nil } func splitUrlAndFragment(s string) (string, string) { diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 38e3a01fd055..ee30b48fbaa8 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -1157,9 +1157,11 @@ func TestCourierChannels(t *testing.T) { channelConfig, err := conf.CourierChannels(ctx) require.NoError(t, err) - require.Len(t, channelConfig, 1) + require.Len(t, channelConfig, 2) assert.Equal(t, channelConfig[0].ID, "phone") assert.NotEmpty(t, channelConfig[0].RequestConfig) + assert.Equal(t, channelConfig[1].ID, "email") + assert.NotEmpty(t, channelConfig[1].SMTPConfig) }) t.Run("case=defaults", func(t *testing.T) { diff --git a/driver/config/stub/.kratos.courier.channels.yaml b/driver/config/stub/.kratos.courier.channels.yaml index 9f4cdcd6de94..16b116ba276f 100644 --- a/driver/config/stub/.kratos.courier.channels.yaml +++ b/driver/config/stub/.kratos.courier.channels.yaml @@ -1,4 +1,6 @@ courier: + smtp: + connection_uri: smtp://username:password@smtp.example.com:587 channels: - id: phone request_config: From dfc931f65fc27662864eb415e55e43d00093abc0 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:20:22 +0000 Subject: [PATCH 022/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e3a3f91c03..aeacc18ce9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-02-23)](#2024-02-23) +- [ (2024-03-04)](#2024-03-04) - [Bug Fixes](#bug-fixes) - [Features](#features) - [Tests](#tests) @@ -321,7 +321,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-02-23) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-04) ### Bug Fixes @@ -339,6 +339,9 @@ - Ignore decrypt errors in WithDeclassifiedCredentials ([#3731](https://github.com/ory/kratos/issues/3731)) ([8f5192f](https://github.com/ory/kratos/commit/8f5192fbb74c4b952029a6856284de8d59027770)) +- Make sure emails can still be sent with SMS enabled + ([#3795](https://github.com/ory/kratos/issues/3795)) + ([7c68c5a](https://github.com/ory/kratos/commit/7c68c5aa69ed76a84a37a37a3555277ddc772cf8)) - Prevent SMTP URL leak on unparsable URL ([#3770](https://github.com/ory/kratos/issues/3770)) ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) @@ -354,6 +357,8 @@ - Add transient payloads to all flows ([#3738](https://github.com/ory/kratos/issues/3738)) ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) +- Add twitter SSO ([#3778](https://github.com/ory/kratos/issues/3778)) + ([930fb19](https://github.com/ory/kratos/commit/930fb19842e527e5e9c415efa983b36e02829516)) ### Tests From e6db689e0de41067e6e78889c3dab9637a96236e Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:05:39 +0100 Subject: [PATCH 023/262] fix: show error page on identity mismatch (#3790) --- continuity/container.go | 2 +- selfservice/flow/settings/flow.go | 4 ++-- selfservice/flow/settings/handler_test.go | 12 ++++++------ selfservice/strategy/password/settings_test.go | 10 +++++----- selfservice/strategy/profile/strategy_test.go | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/continuity/container.go b/continuity/container.go index 9b2d434b859a..823942414555 100644 --- a/continuity/container.go +++ b/continuity/container.go @@ -63,7 +63,7 @@ func (c *Container) Valid(identity uuid.UUID) error { } if identity != uuid.Nil && pointerx.Deref(c.IdentityID) != identity { - return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You must restart the flow because the resumable session was initiated by another person.")) + return errors.WithStack(herodot.ErrForbidden.WithReasonf("The flow has been blocked for security reasons because it was initiated by another person..")) } return nil diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go index 25632cd2e93e..7b0c14bc347e 100644 --- a/selfservice/flow/settings/flow.go +++ b/selfservice/flow/settings/flow.go @@ -199,8 +199,8 @@ func (f *Flow) Valid(s *session.Session) error { } if f.IdentityID != s.Identity.ID { - return errors.WithStack(herodot.ErrBadRequest.WithID(text.ErrIDInitiatedBySomeoneElse).WithReasonf( - "You must restart the flow because the resumable session was initiated by another person.")) + return errors.WithStack(herodot.ErrForbidden.WithID(text.ErrIDInitiatedBySomeoneElse).WithReasonf( + "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.")) } return nil diff --git a/selfservice/flow/settings/handler_test.go b/selfservice/flow/settings/handler_test.go index a24e598c64aa..35d34fd735fc 100644 --- a/selfservice/flow/settings/handler_test.go +++ b/selfservice/flow/settings/handler_test.go @@ -544,8 +544,8 @@ func TestHandler(t *testing.T) { require.NoError(t, json.Unmarshal(body, &f)) actual, res := testhelpers.SettingsMakeRequest(t, true, false, &f, user2, `{"method":"not-exists"}`) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Equal(t, "You must restart the flow because the resumable session was initiated by another person.", gjson.Get(actual, "ui.messages.0.text").String(), actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Equal(t, "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.", gjson.Get(actual, "error.reason").String(), actual) }) t.Run("type=spa", func(t *testing.T) { @@ -556,8 +556,8 @@ func TestHandler(t *testing.T) { require.NoError(t, json.Unmarshal(body, &f)) actual, res := testhelpers.SettingsMakeRequest(t, false, true, &f, user2, `{"method":"not-exists"}`) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Equal(t, "You must restart the flow because the resumable session was initiated by another person.", gjson.Get(actual, "ui.messages.0.text").String(), actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Equal(t, "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.", gjson.Get(actual, "error.reason").String(), actual) }) t.Run("type=browser", func(t *testing.T) { @@ -568,8 +568,8 @@ func TestHandler(t *testing.T) { require.NoError(t, json.Unmarshal(body, &f)) actual, res := testhelpers.SettingsMakeRequest(t, false, false, &f, user2, `{"method":"not-exists"}`) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Equal(t, "You must restart the flow because the resumable session was initiated by another person.", gjson.Get(actual, "ui.messages.0.text").String(), actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Equal(t, "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.", gjson.Get(actual, "error.reason").String(), actual) }) }) diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go index 3c5a3c9f9615..a4ee7e6c7fa0 100644 --- a/selfservice/strategy/password/settings_test.go +++ b/selfservice/strategy/password/settings_test.go @@ -202,8 +202,8 @@ func TestSettings(t *testing.T) { values.Set("method", "password") values.Set("password", x.NewUUID().String()) actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values)) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=spa", func(t *testing.T) { @@ -212,8 +212,8 @@ func TestSettings(t *testing.T) { values.Set("method", "password") values.Set("password", x.NewUUID().String()) actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, values.Encode()) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=browser", func(t *testing.T) { @@ -223,7 +223,7 @@ func TestSettings(t *testing.T) { values.Set("password", x.NewUUID().String()) actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser2, values.Encode()) assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Contains(t, gjson.Get(actual, "reason").String(), "initiated by someone else", "%s", actual) }) }) diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go index f67407fe799f..7d0c831711c3 100644 --- a/selfservice/strategy/profile/strategy_test.go +++ b/selfservice/strategy/profile/strategy_test.go @@ -275,8 +275,8 @@ func TestStrategyTraits(t *testing.T) { values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values)) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=spa", func(t *testing.T) { @@ -284,8 +284,8 @@ func TestStrategyTraits(t *testing.T) { values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, testhelpers.EncodeFormAsJSON(t, true, values)) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=browser", func(t *testing.T) { @@ -294,7 +294,7 @@ func TestStrategyTraits(t *testing.T) { values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser2, values.Encode()) assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Contains(t, gjson.Get(actual, "reason").String(), "initiated by someone else", "%s", actual) }) }) From 7017490caa9c70e22d5c626773c0266521813ff5 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Mon, 4 Mar 2024 13:05:59 +0100 Subject: [PATCH 024/262] fix: audit issues (#3797) --- .schema/openapi/templates/go/api.mustache | 2 +- cmd/daemon/serve.go | 4 +- courier/smtp.go | 13 +++-- driver/config/config.go | 2 +- internal/client-go/api_courier.go | 4 +- internal/client-go/api_frontend.go | 60 ++++++++++----------- internal/client-go/api_identity.go | 36 ++++++------- internal/client-go/api_metadata.go | 6 +-- internal/httpclient/api_courier.go | 4 +- internal/httpclient/api_frontend.go | 60 ++++++++++----------- internal/httpclient/api_identity.go | 36 ++++++------- internal/httpclient/api_metadata.go | 6 +-- schema/handler.go | 2 +- schema/schema.go | 2 +- selfservice/strategy/oidc/error.go | 2 +- selfservice/strategy/oidc/provider_auth0.go | 2 +- 16 files changed, 123 insertions(+), 118 deletions(-) diff --git a/.schema/openapi/templates/go/api.mustache b/.schema/openapi/templates/go/api.mustache index 6ee735a7ffdd..e5f00dcbfa95 100644 --- a/.schema/openapi/templates/go/api.mustache +++ b/.schema/openapi/templates/go/api.mustache @@ -321,7 +321,7 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go index e68523243dd5..fb7557033e01 100644 --- a/cmd/daemon/serve.go +++ b/cmd/daemon/serve.go @@ -116,7 +116,7 @@ func servePublic(r driver.Registry, cmd *cobra.Command, eg *errgroup.Group, slOp n.UseFunc(x.CleanPath) // Prevent double slashes from breaking CSRF. r.WithCSRFHandler(csrf) - n.UseHandler(r.CSRFHandler()) + n.UseHandler(http.MaxBytesHandler(r.CSRFHandler(), 5*1024*1024 /* 5 MB */)) // Disable CSRF for these endpoints csrf.DisablePath(healthx.AliveCheckPath) @@ -199,7 +199,7 @@ func serveAdmin(r driver.Registry, cmd *cobra.Command, eg *errgroup.Group, slOpt r.RegisterAdminRoutes(ctx, router) r.PrometheusManager().RegisterRouter(router.Router) - n.UseHandler(router) + n.UseHandler(http.MaxBytesHandler(router, 5*1024*1024 /* 5 MB */)) certs := c.GetTLSCertificatesForAdmin(ctx) var handler http.Handler = n diff --git a/courier/smtp.go b/courier/smtp.go index 2aa232132930..7895fd05f762 100644 --- a/courier/smtp.go +++ b/courier/smtp.go @@ -66,6 +66,13 @@ func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, erro serverName = uri.Hostname() } + tlsConfig := &tls.Config{ + InsecureSkipVerify: sslSkipVerify, //#nosec G402 -- This is ok (and required!) because it is configurable and disabled by default. + Certificates: tlsCertificates, + ServerName: serverName, + MinVersion: tls.VersionTLS12, + } + // SMTP schemes // smtp: smtp clear text (with uri parameter) or with StartTLS (enforced by default) // smtps: smtp with implicit TLS (recommended way in 2021 to avoid StartTLS downgrade attacks @@ -75,14 +82,12 @@ func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, erro // Enforcing StartTLS by default for security best practices (config review, etc.) skipStartTLS, _ := strconv.ParseBool(uri.Query().Get("disable_starttls")) if !skipStartTLS { - //#nosec G402 -- This is ok (and required!) because it is configurable and disabled by default. - dialer.TLSConfig = &tls.Config{InsecureSkipVerify: sslSkipVerify, Certificates: tlsCertificates, ServerName: serverName} + dialer.TLSConfig = tlsConfig // Enforcing StartTLS dialer.StartTLSPolicy = gomail.MandatoryStartTLS } case "smtps": - //#nosec G402 -- This is ok (and required!) because it is configurable and disabled by default. - dialer.TLSConfig = &tls.Config{InsecureSkipVerify: sslSkipVerify, Certificates: tlsCertificates, ServerName: serverName} + dialer.TLSConfig = tlsConfig dialer.SSL = true } diff --git a/driver/config/config.go b/driver/config/config.go index ad2d414fdfa3..92ba9cee38e2 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -472,7 +472,7 @@ func (p *Config) validateIdentitySchemas(ctx context.Context) error { } defer resource.Close() - schema, err := io.ReadAll(resource) + schema, err := io.ReadAll(io.LimitReader(resource, 1024*1024)) if err != nil { return errors.WithStack(err) } diff --git a/internal/client-go/api_courier.go b/internal/client-go/api_courier.go index a7e43bcd183e..91bcc08025eb 100644 --- a/internal/client-go/api_courier.go +++ b/internal/client-go/api_courier.go @@ -152,7 +152,7 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -317,7 +317,7 @@ func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/client-go/api_frontend.go b/internal/client-go/api_frontend.go index 4e27c89f1f12..cfb87b55902a 100644 --- a/internal/client-go/api_frontend.go +++ b/internal/client-go/api_frontend.go @@ -1087,7 +1087,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1231,7 +1231,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1380,7 +1380,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1550,7 +1550,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1701,7 +1701,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1856,7 +1856,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2032,7 +2032,7 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2162,7 +2162,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2315,7 +2315,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2464,7 +2464,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2592,7 +2592,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2729,7 +2729,7 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2878,7 +2878,7 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3015,7 +3015,7 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3169,7 +3169,7 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3339,7 +3339,7 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3512,7 +3512,7 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3680,7 +3680,7 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3863,7 +3863,7 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4046,7 +4046,7 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4182,7 +4182,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4334,7 +4334,7 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4479,7 +4479,7 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4672,7 +4672,7 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4863,7 +4863,7 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5036,7 +5036,7 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5187,7 +5187,7 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5381,7 +5381,7 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5598,7 +5598,7 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5808,7 +5808,7 @@ func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index c3c361d16ad4..97354d5c0d0b 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -412,7 +412,7 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -565,7 +565,7 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -717,7 +717,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -877,7 +877,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1024,7 +1024,7 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1155,7 +1155,7 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1280,7 +1280,7 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1425,7 +1425,7 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1566,7 +1566,7 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1731,7 +1731,7 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1853,7 +1853,7 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2008,7 +2008,7 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2229,7 +2229,7 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2370,7 +2370,7 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2537,7 +2537,7 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2720,7 +2720,7 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2866,7 +2866,7 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3032,7 +3032,7 @@ func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/client-go/api_metadata.go b/internal/client-go/api_metadata.go index 0ea297189e2f..e5ac1a854d5d 100644 --- a/internal/client-go/api_metadata.go +++ b/internal/client-go/api_metadata.go @@ -172,7 +172,7 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -281,7 +281,7 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -397,7 +397,7 @@ func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/api_courier.go b/internal/httpclient/api_courier.go index a7e43bcd183e..91bcc08025eb 100644 --- a/internal/httpclient/api_courier.go +++ b/internal/httpclient/api_courier.go @@ -152,7 +152,7 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -317,7 +317,7 @@ func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/api_frontend.go b/internal/httpclient/api_frontend.go index 4e27c89f1f12..cfb87b55902a 100644 --- a/internal/httpclient/api_frontend.go +++ b/internal/httpclient/api_frontend.go @@ -1087,7 +1087,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1231,7 +1231,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1380,7 +1380,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1550,7 +1550,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1701,7 +1701,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1856,7 +1856,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2032,7 +2032,7 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2162,7 +2162,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2315,7 +2315,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2464,7 +2464,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2592,7 +2592,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2729,7 +2729,7 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2878,7 +2878,7 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3015,7 +3015,7 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3169,7 +3169,7 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3339,7 +3339,7 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3512,7 +3512,7 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3680,7 +3680,7 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3863,7 +3863,7 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4046,7 +4046,7 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4182,7 +4182,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4334,7 +4334,7 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4479,7 +4479,7 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4672,7 +4672,7 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4863,7 +4863,7 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5036,7 +5036,7 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5187,7 +5187,7 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5381,7 +5381,7 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5598,7 +5598,7 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5808,7 +5808,7 @@ func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index c3c361d16ad4..97354d5c0d0b 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -412,7 +412,7 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -565,7 +565,7 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -717,7 +717,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -877,7 +877,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1024,7 +1024,7 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1155,7 +1155,7 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1280,7 +1280,7 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1425,7 +1425,7 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1566,7 +1566,7 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1731,7 +1731,7 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1853,7 +1853,7 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2008,7 +2008,7 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2229,7 +2229,7 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2370,7 +2370,7 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2537,7 +2537,7 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2720,7 +2720,7 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2866,7 +2866,7 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3032,7 +3032,7 @@ func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/api_metadata.go b/internal/httpclient/api_metadata.go index 0ea297189e2f..e5ac1a854d5d 100644 --- a/internal/httpclient/api_metadata.go +++ b/internal/httpclient/api_metadata.go @@ -172,7 +172,7 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -281,7 +281,7 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -397,7 +397,7 @@ func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/schema/handler.go b/schema/handler.go index 3cbcc529417e..e154ae75d699 100644 --- a/schema/handler.go +++ b/schema/handler.go @@ -218,7 +218,7 @@ func (h *Handler) getAll(w http.ResponseWriter, r *http.Request, ps httprouter.P return } - raw, err := io.ReadAll(src) + raw, err := io.ReadAll(io.LimitReader(src, 1024*1024)) _ = src.Close() if err != nil { h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The file for this JSON Schema ID could not be found or opened. This is a configuration issue.").WithDebugf("%+v", err))) diff --git a/schema/schema.go b/schema/schema.go index 305064edf9e3..69b6bbca7332 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -83,7 +83,7 @@ func GetKeysInOrder(ctx context.Context, schemaRef string) ([]string, error) { if err != nil { return nil, errors.WithStack(err) } - schema, err := io.ReadAll(sio) + schema, err := io.ReadAll(io.LimitReader(sio, 1024*1024)) if err != nil { return nil, errors.WithStack(err) } diff --git a/selfservice/strategy/oidc/error.go b/selfservice/strategy/oidc/error.go index d0d4b14ab1fc..8fe984655a9a 100644 --- a/selfservice/strategy/oidc/error.go +++ b/selfservice/strategy/oidc/error.go @@ -28,7 +28,7 @@ func logUpstreamError(l *logrusx.Logger, resp *http.Response) error { return nil } - body, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) if err != nil { l = l.WithError(err) } diff --git a/selfservice/strategy/oidc/provider_auth0.go b/selfservice/strategy/oidc/provider_auth0.go index 15f332d5cfa8..a4c9ee46e1ab 100644 --- a/selfservice/strategy/oidc/provider_auth0.go +++ b/selfservice/strategy/oidc/provider_auth0.go @@ -102,7 +102,7 @@ func (g *ProviderAuth0) Claims(ctx context.Context, exchange *oauth2.Token, quer } // Once auth0 fixes this bug, all this workaround can be removed. - b, err := io.ReadAll(resp.Body) + b, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } From f8fbb006c09629e9c04565b43ef39e017fa99750 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:49:32 +0000 Subject: [PATCH 025/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeacc18ce9a6..9202a43b1727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -336,6 +336,8 @@ - Add sms mfa via parameter to spec ([#3766](https://github.com/ory/kratos/issues/3766)) ([b291c95](https://github.com/ory/kratos/commit/b291c959c18c72f5edc55607ab23b4592faf8d53)) +- Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) + ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) - Ignore decrypt errors in WithDeclassifiedCredentials ([#3731](https://github.com/ory/kratos/issues/3731)) ([8f5192f](https://github.com/ory/kratos/commit/8f5192fbb74c4b952029a6856284de8d59027770)) @@ -345,6 +347,9 @@ - Prevent SMTP URL leak on unparsable URL ([#3770](https://github.com/ory/kratos/issues/3770)) ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) +- Show error page on identity mismatch + ([#3790](https://github.com/ory/kratos/issues/3790)) + ([e6db689](https://github.com/ory/kratos/commit/e6db689e0de41067e6e78889c3dab9637a96236e)) - Test assertions on declassifying OIDC tokens ([#3773](https://github.com/ory/kratos/issues/3773)) ([7f8a7f1](https://github.com/ory/kratos/commit/7f8a7f142a91c8c74f32eadb41224fc4f69c2109)) From 04390bee426befe51af2ee8177afabaa9ce4fa80 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Wed, 6 Mar 2024 15:34:15 +0100 Subject: [PATCH 026/262] feat: send OIDC claim keys to tracing (#3798) --- internal/client-go/go.sum | 1 + selfservice/strategy/oidc/strategy.go | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 5289b2a9b96b..5283791cd85d 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -15,6 +15,8 @@ import ( "path/filepath" "strings" + "golang.org/x/exp/maps" + "github.com/ory/x/urlx" "go.opentelemetry.io/otel/attribute" @@ -384,10 +386,12 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt var ( code = stringsx.Coalesce(r.URL.Query().Get("code"), r.URL.Query().Get("authCode")) pid = ps.ByName("provider") + err error ) - ctx := r.Context() - ctx = context.WithValue(ctx, httprouter.ParamsKey, ps) + ctx := context.WithValue(r.Context(), httprouter.ParamsKey, ps) + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.ExchangeCode") + defer otelx.End(span, &err) r = r.WithContext(ctx) req, cntnr, err := s.ValidateCallback(w, r) @@ -447,11 +451,13 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt } } - if err := claims.Validate(); err != nil { + if err = claims.Validate(); err != nil { s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) return } + span.SetAttributes(attribute.StringSlice("claims", maps.Keys(claims.RawClaims))) + switch a := req.(type) { case *login.Flow: if ff, err := s.processLogin(w, r, a, et, claims, provider, cntnr); err != nil { From ca7cd23dd75efd8b0a4b4dd45e37aa1a68865c98 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:36:04 +0000 Subject: [PATCH 027/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From ecbd1e36ebe467b7109e899c6f034a81cd07bf4c Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:18:55 +0000 Subject: [PATCH 028/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9202a43b1727..ecb8b45fa3b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-03-04)](#2024-03-04) +- [ (2024-03-06)](#2024-03-06) - [Bug Fixes](#bug-fixes) - [Features](#features) - [Tests](#tests) @@ -321,7 +321,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-04) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-06) ### Bug Fixes @@ -364,6 +364,9 @@ ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) - Add twitter SSO ([#3778](https://github.com/ory/kratos/issues/3778)) ([930fb19](https://github.com/ory/kratos/commit/930fb19842e527e5e9c415efa983b36e02829516)) +- Send OIDC claim keys to tracing + ([#3798](https://github.com/ory/kratos/issues/3798)) + ([04390be](https://github.com/ory/kratos/commit/04390bee426befe51af2ee8177afabaa9ce4fa80)) ### Tests From 0b32ce113be47aa724d3468062ced09f8f60c52a Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Thu, 7 Mar 2024 16:52:56 +0100 Subject: [PATCH 029/262] fix: missing indices and foreign keys (#3800) --- .github/workflows/ci.yaml | 5 ++-- Makefile | 2 +- go.mod | 7 ++++-- go.sum | 8 +++--- .../sql/identity/persister_identity.go | 6 ++++- persistence/sql/migratest/migration_test.go | 25 ++++++++----------- ...cookie_flow_request_url.cockroach.down.sql | 5 ---- ...000000002_cookie_flow_request_url.down.sql | 1 - ...overy_codes_flow_id_idx.cockroach.down.sql | 5 ++++ ...entity_recovery_codes_flow_id_idx.down.sql | 5 ++++ ..._recovery_codes_flow_id_idx.mysql.down.sql | 6 +++++ ...ty_recovery_codes_flow_id_idx.mysql.up.sql | 16 ++++++++++++ ...y_recovery_codes_flow_id_idx.sqlite.up.sql | 5 ++++ ...identity_recovery_codes_flow_id_idx.up.sql | 8 ++++++ 14 files changed, 73 insertions(+), 31 deletions(-) delete mode 100644 persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.cockroach.down.sql delete mode 100644 persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.down.sql create mode 100644 persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.cockroach.down.sql create mode 100644 persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.down.sql create mode 100644 persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.down.sql create mode 100644 persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.up.sql create mode 100644 persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.sqlite.up.sql create mode 100644 persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.up.sql diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9e4a3fd1d947..6d4ed3dc155b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -88,13 +88,12 @@ jobs: - run: npm install name: Install node deps - name: Run golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v4 env: GOGC: 100 with: args: --timeout 10m0s - version: v1.55.2 - skip-go-installation: true + version: v1.56.2 skip-pkg-cache: true - name: Build Kratos run: make install diff --git a/Makefile b/Makefile index 1775d09b25ea..61e4284d3994 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ docs/swagger: npx @redocly/openapi-cli preview-docs spec/swagger.json .bin/golangci-lint: Makefile - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.55.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.56.2 .bin/hydra: Makefile bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v2.2.0-rc.3 diff --git a/go.mod b/go.mod index 41effb40c1c8..aff3151ea5c2 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,11 @@ go 1.21 replace ( github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb - github.com/gorilla/sessions => github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 + // https://github.com/gobuffalo/pop/pull/833 + github.com/gobuffalo/pop/v6 => github.com/alnr/pop/v6 v6.1.2-0.20240220141536-653aad67c0c2 + + github.com/gorilla/sessions => github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.16 // Use the internal httpclient which can be generated in this codebase but mark it as the @@ -74,7 +77,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.614 + github.com/ory/x v0.0.616 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 9f68475d07fc..e6f0867375bc 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alnr/pop/v6 v6.1.2-0.20240220141536-653aad67c0c2 h1:GcIj2UDicQcj5xPwdpyYzqFP3GITJFzuoRyvqZTHz1c= +github.com/alnr/pop/v6 v6.1.2-0.20240220141536-653aad67c0c2/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -329,8 +331,6 @@ github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/V github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= github.com/gobuffalo/plush/v4 v4.1.18 h1:bnPjdMTEUQHqj9TNX2Ck3mxEXYZa+0nrFMNM07kpX9g= github.com/gobuffalo/plush/v4 v4.1.18/go.mod h1:xi2tJIhFI4UdzIL8sxZtzGYOd2xbBpcFbLZlIPGGZhU= -github.com/gobuffalo/pop/v6 v6.1.2-0.20230318123913-c85387acc9a0 h1:+LF3Enal3HZ+rFmaLZfBRNHKqtnoA0d8jk0Iio8InZM= -github.com/gobuffalo/pop/v6 v6.1.2-0.20230318123913-c85387acc9a0/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= @@ -821,8 +821,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.614 h1:amqUBxoY5Z0fN+WqH1sLLtGuJa5GYOBo76LyrwJC0dc= -github.com/ory/x v0.0.614/go.mod h1:uH065puz8neija0neqwIN3PmXXfDsB9VbZTZ20Znoos= +github.com/ory/x v0.0.616 h1:iaojp7MvFW1cdirSZFK/XeuJvyhUEVXQdY61bmIOkzk= +github.com/ory/x v0.0.616/go.mod h1:Fqxxc1Ks6a4vZuqWwr6TYAeUDh2SAvxXyrk9N7Hidbo= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index b07275918e17..a22b62827f8a 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -960,8 +960,12 @@ func (p *IdentityPersister) DeleteIdentity(ctx context.Context, id uuid.UUID) (e attribute.Stringer("network.id", p.NetworkID(ctx)))) defer otelx.End(span, &err) + tableName := new(identity.Identity).TableName(ctx) + if p.c.Dialect.Name() == "cockroach" { + tableName += "@primary" + } nid := p.NetworkID(ctx) - count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE id = ? AND nid = ?", new(identity.Identity).TableName(ctx)), + count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE id = ? AND nid = ?", tableName), id, nid, ).ExecWithCount() diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 93513badc4c7..bf727683248b 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -108,7 +108,7 @@ func TestMigrations_Cockroach(t *testing.T) { func testDatabase(t *testing.T, db string, c *pop.Connection) { ctx := context.Background() - l := logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel)) + l := logrusx.New("", "", logrusx.ForceLevel(logrus.DebugLevel)) t.Logf("Cleaning up before migrations") _ = os.Remove("../migrations/sql/schema.sql") @@ -130,15 +130,14 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { } t.Logf("URL: %s", url) - t.Run("suite=up", func(t *testing.T) { - tm, err := popx.NewMigrationBox( - os.DirFS("../migrations/sql"), - popx.NewMigrator(c, logrusx.New("", "", logrusx.ForceLevel(logrus.DebugLevel)), nil, 1*time.Minute), - popx.WithTestdata(t, os.DirFS("./testdata")), - ) - require.NoError(t, err) - require.NoError(t, tm.Up(ctx)) - }) + tm, err := popx.NewMigrationBox( + os.DirFS("../migrations/sql"), + popx.NewMigrator(c, l, nil, 1*time.Minute), + popx.WithTestdata(t, os.DirFS("./testdata")), + ) + require.NoError(t, err) + tm.DumpMigrations = true + require.NoError(t, tm.Up(ctx)) t.Run("suite=fixtures", func(t *testing.T) { wg := &sync.WaitGroup{} @@ -423,8 +422,6 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { }) }) - t.Run("suite=down", func(t *testing.T) { - tm := popx.NewTestMigrator(t, c, os.DirFS("../migrations/sql"), os.DirFS("./testdata"), l) - require.NoError(t, tm.Down(ctx, -1)) - }) + tm.DumpMigrations = false + require.NoError(t, tm.Down(ctx, -1)) } diff --git a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.cockroach.down.sql b/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.cockroach.down.sql deleted file mode 100644 index af709521a07a..000000000000 --- a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.cockroach.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE selfservice_login_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_recovery_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_registration_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_settings_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_verification_flows ALTER COLUMN request_url TYPE VARCHAR(1024); diff --git a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.down.sql b/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.down.sql deleted file mode 100644 index 14e0ab5aae32..000000000000 --- a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.down.sql +++ /dev/null @@ -1 +0,0 @@ --- nothing to do diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.cockroach.down.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.cockroach.down.sql new file mode 100644 index 000000000000..1daec47013bc --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.cockroach.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS identity_login_codes@identity_login_codes_identity_id_idx; +DROP INDEX IF EXISTS identity_login_codes@identity_login_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_recovery_codes@identity_recovery_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_registration_codes@identity_registration_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_verification_codes@identity_verification_codes_flow_id_idx; diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.down.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.down.sql new file mode 100644 index 000000000000..73d7df82baf1 --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS identity_login_codes.identity_login_codes_identity_id_idx; +DROP INDEX IF EXISTS identity_login_codes.identity_login_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_recovery_codes.identity_recovery_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_registration_codes.identity_registration_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_verification_codes.identity_verification_codes_flow_id_idx; diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.down.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.down.sql new file mode 100644 index 000000000000..838a77061a15 --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE `identity_recovery_codes` + DROP FOREIGN KEY `identity_recovery_codes_identity_id_fk`, + ADD CONSTRAINT `identity_recovery_tokens_identity_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +ALTER TABLE `identity_login_codes` + DROP FOREIGN KEY `identity_login_codes_identity_id_fk`; diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.up.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.up.sql new file mode 100644 index 000000000000..c1e35805ff5f --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.up.sql @@ -0,0 +1,16 @@ +-- This FK was previously misnamed. +ALTER TABLE `identity_recovery_codes` + DROP FOREIGN KEY `identity_recovery_tokens_identity_id_fk`, + ADD CONSTRAINT `identity_recovery_codes_identity_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- Missing FK +ALTER TABLE `identity_login_codes` + ADD CONSTRAINT `identity_login_codes_identity_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- MySQL has created the remaining indices automatically together with the foreign key constraints. + +-- CREATE INDEX identity_login_codes_identity_id_idx ON identity_login_codes (identity_id ASC); +-- CREATE INDEX identity_login_codes_flow_id_idx ON identity_login_codes (selfservice_login_flow_id ASC); +-- CREATE INDEX identity_registration_codes_flow_id_idx ON identity_registration_codes (selfservice_registration_flow_id ASC); +-- CREATE INDEX identity_recovery_codes_flow_id_idx ON identity_recovery_codes (selfservice_recovery_flow_id ASC); +-- CREATE INDEX identity_verification_codes_flow_id_idx ON identity_verification_codes (selfservice_verification_flow_id ASC); diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.sqlite.up.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.sqlite.up.sql new file mode 100644 index 000000000000..61741fcba534 --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.sqlite.up.sql @@ -0,0 +1,5 @@ +CREATE INDEX IF NOT EXISTS identity_login_codes_identity_id_idx ON identity_login_codes (identity_id ASC); +CREATE INDEX IF NOT EXISTS identity_login_codes_flow_id_idx ON identity_login_codes (selfservice_login_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_registration_codes_flow_id_idx ON identity_registration_codes (selfservice_registration_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_flow_id_idx ON identity_recovery_codes (selfservice_recovery_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_verification_codes_flow_id_idx ON identity_verification_codes (selfservice_verification_flow_id ASC); diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.up.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.up.sql new file mode 100644 index 000000000000..6c3e7e4c9b8a --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE identity_login_codes + ADD CONSTRAINT identity_login_codes_identity_id_fk FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE ON UPDATE RESTRICT; + +CREATE INDEX IF NOT EXISTS identity_login_codes_identity_id_idx ON identity_login_codes (identity_id ASC); +CREATE INDEX IF NOT EXISTS identity_login_codes_flow_id_idx ON identity_login_codes (selfservice_login_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_registration_codes_flow_id_idx ON identity_registration_codes (selfservice_registration_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_flow_id_idx ON identity_recovery_codes (selfservice_recovery_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_verification_codes_flow_id_idx ON identity_verification_codes (selfservice_verification_flow_id ASC); From c9dcce5a41137937df1aad7ac81170b443740f88 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:23:47 +0100 Subject: [PATCH 030/262] feat: control edge cache ttl (#3808) --- .schemastore/config.schema.json | 8 +++++++- driver/config/config.go | 5 +++++ embedx/config.schema.json | 6 ++++++ internal/client-go/go.sum | 1 + session/handler.go | 7 ++++++- session/handler_test.go | 18 ++++++++++++++---- 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.schemastore/config.schema.json b/.schemastore/config.schema.json index d03c22b37246..829b71bae3fb 100644 --- a/.schemastore/config.schema.json +++ b/.schemastore/config.schema.json @@ -2731,10 +2731,16 @@ "properties": { "cacheable_sessions": { "type": "boolean", - "title": "Enable Ory Sessions caching", + "title": "Enable Ory Session Edge Caching", "description": "If enabled allows Ory Sessions to be cached. Only effective in the Ory Network.", "default": false }, + "cacheable_sessions_max_age": { + "title": "Set Ory Session Edge Caching maximum age", + "description": "Set how long Ory Sessions are cached on the edge. If unset, the session expiry will be used. Only effective in the Ory Network.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$" + }, "use_continue_with_transitions": { "type": "boolean", "title": "Enable new flow transitions using `continue_with` items", diff --git a/driver/config/config.go b/driver/config/config.go index 92ba9cee38e2..78fa3d633854 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -115,6 +115,7 @@ const ( ViperKeySessionTokenizerTemplates = "session.whoami.tokenizer.templates" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" ViperKeySessionWhoAmICaching = "feature_flags.cacheable_sessions" + ViperKeySessionWhoAmICachingMaxAge = "feature_flags.cacheable_sessions_max_age" ViperKeyUseContinueWithTransitions = "feature_flags.use_continue_with_transitions" ViperKeySessionRefreshMinTimeLeft = "session.earliest_possible_extend" ViperKeyCookieSameSite = "cookies.same_site" @@ -1353,6 +1354,10 @@ func (p *Config) SessionWhoAmICaching(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeySessionWhoAmICaching) } +func (p *Config) SessionWhoAmICachingMaxAge(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySessionWhoAmICachingMaxAge, 0) +} + func (p *Config) UseContinueWithTransitions(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeyUseContinueWithTransitions) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 65eacf9cfbd4..6a355e055639 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2736,6 +2736,12 @@ "description": "If enabled allows Ory Sessions to be cached. Only effective in the Ory Network.", "default": false }, + "cacheable_sessions_max_age": { + "title": "Set Ory Session Edge Caching maximum age", + "description": "Set how long Ory Sessions are cached on the edge. If unset, the session expiry will be used. Only effective in the Ory Network.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$" + }, "use_continue_with_transitions": { "type": "boolean", "title": "Enable new flow transitions using `continue_with` items", diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/session/handler.go b/session/handler.go index f69e9c655c67..56bd053e551a 100644 --- a/session/handler.go +++ b/session/handler.go @@ -255,7 +255,12 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, _ httprouter.Pa // Set Cache header only when configured, and when no tokenization is requested. if c.SessionWhoAmICaching(ctx) && len(tokenizeTemplate) == 0 { - w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%d", int64(time.Until(s.ExpiresAt).Seconds()))) + expiry := time.Until(s.ExpiresAt) + if c.SessionWhoAmICachingMaxAge(ctx) > 0 && expiry > c.SessionWhoAmICachingMaxAge(ctx) { + expiry = c.SessionWhoAmICachingMaxAge(ctx) + } + + w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%0.f", expiry.Seconds())) } if err := h.r.SessionManager().RefreshCookie(ctx, w, r, s); err != nil { diff --git a/session/handler_test.go b/session/handler_test.go index dce2f7b05116..cd0a9ddca6c1 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -142,8 +142,9 @@ func TestSessionWhoAmI(t *testing.T) { }) t.Run("case=http methods", func(t *testing.T) { - run := func(t *testing.T, cacheEnabled bool) { + run := func(t *testing.T, cacheEnabled bool, maxAge time.Duration) { conf.MustSet(ctx, config.ViperKeySessionWhoAmICaching, cacheEnabled) + conf.MustSet(ctx, config.ViperKeySessionWhoAmICachingMaxAge, maxAge) client := testhelpers.NewClientWithCookies(t) // No cookie yet -> 401 @@ -153,6 +154,7 @@ func TestSessionWhoAmI(t *testing.T) { if cacheEnabled { assert.NotEmpty(t, res.Header.Get("Ory-Session-Cache-For")) + assert.Equal(t, "60", res.Header.Get("Ory-Session-Cache-For")) } else { assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) } @@ -182,7 +184,11 @@ func TestSessionWhoAmI(t *testing.T) { assert.NotEmpty(t, res.Header.Get("X-Kratos-Authenticated-Identity-Id")) if cacheEnabled { - assert.NotEmpty(t, res.Header.Get("Ory-Session-Cache-For")) + if maxAge > 0 { + assert.Equal(t, fmt.Sprintf("%0.f", maxAge.Seconds()), res.Header.Get("Ory-Session-Cache-For")) + } else { + assert.Equal(t, fmt.Sprintf("%0.f", conf.SessionLifespan(ctx).Seconds()), res.Header.Get("Ory-Session-Cache-For")) + } } else { assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) } @@ -198,11 +204,15 @@ func TestSessionWhoAmI(t *testing.T) { } t.Run("cache disabled", func(t *testing.T) { - run(t, false) + run(t, false, 0) }) t.Run("cache enabled", func(t *testing.T) { - run(t, true) + run(t, true, 0) + }) + + t.Run("cache enabled with max age", func(t *testing.T) { + run(t, true, time.Minute) }) }) From 7f1fd818166a318a734277c1edd59eacf5947b0f Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 8 Mar 2024 12:25:15 +0000 Subject: [PATCH 031/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 0f81b768499a5c9240b34761e90a712f97003c55 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:06:42 +0000 Subject: [PATCH 032/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb8b45fa3b7..d4160b8af7fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-03-06)](#2024-03-06) +- [ (2024-03-08)](#2024-03-08) - [Bug Fixes](#bug-fixes) - [Features](#features) - [Tests](#tests) @@ -321,7 +321,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-06) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-08) ### Bug Fixes @@ -344,6 +344,9 @@ - Make sure emails can still be sent with SMS enabled ([#3795](https://github.com/ory/kratos/issues/3795)) ([7c68c5a](https://github.com/ory/kratos/commit/7c68c5aa69ed76a84a37a37a3555277ddc772cf8)) +- Missing indices and foreign keys + ([#3800](https://github.com/ory/kratos/issues/3800)) + ([0b32ce1](https://github.com/ory/kratos/commit/0b32ce113be47aa724d3468062ced09f8f60c52a)) - Prevent SMTP URL leak on unparsable URL ([#3770](https://github.com/ory/kratos/issues/3770)) ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) @@ -364,6 +367,8 @@ ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) - Add twitter SSO ([#3778](https://github.com/ory/kratos/issues/3778)) ([930fb19](https://github.com/ory/kratos/commit/930fb19842e527e5e9c415efa983b36e02829516)) +- Control edge cache ttl ([#3808](https://github.com/ory/kratos/issues/3808)) + ([c9dcce5](https://github.com/ory/kratos/commit/c9dcce5a41137937df1aad7ac81170b443740f88)) - Send OIDC claim keys to tracing ([#3798](https://github.com/ory/kratos/issues/3798)) ([04390be](https://github.com/ory/kratos/commit/04390bee426befe51af2ee8177afabaa9ce4fa80)) From 0f3d082ad66dc058007463eac247b5a9c0aa2051 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:21:18 +0100 Subject: [PATCH 033/262] chore(deps): bump github.com/lestrrat-go/jwx from 1.2.28 to 1.2.29 (#3812) Bumps [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) from 1.2.28 to 1.2.29. - [Release notes](https://github.com/lestrrat-go/jwx/releases) - [Changelog](https://github.com/lestrrat-go/jwx/blob/v1.2.29/Changes) - [Commits](https://github.com/lestrrat-go/jwx/compare/v1.2.28...v1.2.29) --- updated-dependencies: - dependency-name: github.com/lestrrat-go/jwx dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 32 +++++++++++++++++--------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index aff3151ea5c2..04a6ea917423 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/julienschmidt/httprouter v1.3.0 github.com/knadh/koanf/parsers/json v0.1.0 github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7 - github.com/lestrrat-go/jwx v1.2.28 // indirect + github.com/lestrrat-go/jwx v1.2.29 // indirect github.com/luna-duclos/instrumentedsql v1.1.3 github.com/mailhog/MailHog v1.0.1 github.com/mattn/goveralls v0.0.7 @@ -90,7 +90,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.14.3 github.com/tidwall/sjson v1.2.5 github.com/urfave/negroni v1.0.0 @@ -99,9 +99,9 @@ require ( go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/sdk v1.21.0 go.opentelemetry.io/otel/trace v1.22.0 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/net v0.20.0 + golang.org/x/net v0.21.0 golang.org/x/oauth2 v0.16.0 golang.org/x/sync v0.5.0 golang.org/x/text v0.14.0 @@ -308,8 +308,8 @@ require ( go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/tools v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index e6f0867375bc..7db827552f74 100644 --- a/go.sum +++ b/go.sum @@ -672,8 +672,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx v1.2.28 h1:uadI6o0WpOVrBSf498tRXZIwPpEtLnR9CvqPFXeI5sA= -github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8IDQdIazP8= +github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= @@ -964,8 +964,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -976,8 +977,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/t-k/fluent-logger-golang v1.0.0 h1:4IQzY+/l66Zkkhk9eB3LwF9vPkgKHJ1rpYdrRiap0EI= @@ -1108,9 +1110,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1205,8 +1207,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1320,9 +1322,9 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1334,9 +1336,9 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 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.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 3621411dc4386d841bc6766a5ab8d03e65812073 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Mon, 11 Mar 2024 11:31:23 +0100 Subject: [PATCH 034/262] feat: PassKeys with Resident Keys and two-step registration (#3748) BREAKING CHANGES: This feature enables two-step registration per default. Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To disable two-step registration, set `selfservice.flows.registration.enable_legacy_flow` to `true`. This value defaults to `false`. --- cmd/clidoc/main.go | 6 + .../all-strategies/identity.schema.json | 57 ++ .../kratos/all-strategies/kratos.yml | 120 ++++ .../kratos/passkey/identity.schema.json | 34 + contrib/quickstart/kratos/passkey/kratos.yml | 107 ++++ courier/message.go | 2 +- courier/test/persistence.go | 6 +- driver/config/config.go | 34 +- driver/config/config_test.go | 2 +- driver/registry_default.go | 6 + driver/registry_default_hooks.go | 9 + driver/registry_default_test.go | 30 +- embedx/config.schema.json | 76 +++ embedx/identity_extension.schema.json | 9 + identity/credentials.go | 6 + identity/credentials_webauthn.go | 34 +- identity/credentials_webauthn_test.go | 20 + identity/identity.go | 22 +- identity/pool.go | 5 +- identity/test/pool.go | 61 +- internal/client-go/.openapi-generator/FILES | 6 + internal/client-go/README.md | 3 + internal/client-go/go.sum | 1 + internal/client-go/model_ui_node.go | 2 +- .../model_ui_node_input_attributes.go | 37 ++ ...l_update_login_flow_with_passkey_method.go | 182 ++++++ ...e_registration_flow_with_passkey_method.go | 249 ++++++++ ...pdate_settings_flow_with_passkey_method.go | 219 +++++++ internal/httpclient/.openapi-generator/FILES | 6 + internal/httpclient/README.md | 3 + internal/httpclient/model_ui_node.go | 2 +- .../model_ui_node_input_attributes.go | 37 ++ ...l_update_login_flow_with_passkey_method.go | 182 ++++++ ...e_registration_flow_with_passkey_method.go | 249 ++++++++ ...pdate_settings_flow_with_passkey_method.go | 219 +++++++ internal/registrationhelpers/helpers.go | 1 + internal/testhelpers/selfservice_settings.go | 3 +- internal/testhelpers/session.go | 2 +- .../sql/identity/persister_identity.go | 47 +- ...00000000_credential_types_passkey.down.sql | 1 + ...1100000000_credential_types_passkey.up.sql | 3 + ...tials_user_handle_index.cockroach.down.sql | 1 + ...entials_user_handle_index.cockroach.up.sql | 3 + ...ity_credentials_user_handle_index.down.sql | 0 ...ntity_credentials_user_handle_index.up.sql | 0 schema/extension.go | 20 +- .../flow/login/extension_identifier_label.go | 1 + .../login/extension_identifier_label_test.go | 7 + selfservice/flow/login/flow.go | 2 + selfservice/flow/login/sort.go | 1 + selfservice/flow/registration/sort.go | 4 +- selfservice/hook/hooks.go | 11 +- selfservice/hook/two_step_registration.go | 58 ++ .../strategy/code/code_registration.go | 2 +- selfservice/strategy/code/strategy.go | 18 + .../strategy/code/strategy_registration.go | 4 +- .../code/strategy_registration_test.go | 5 +- selfservice/strategy/oidc/provider_config.go | 2 +- .../passkey/.schema/login.schema.json | 16 + .../passkey/.schema/registration.schema.json | 23 + .../passkey/.schema/settings.schema.json | 20 + ...sswordless-case=passkey_button_exists.json | 96 +++ ...resh_passwordless_credentials-browser.json | 87 +++ ...=refresh_passwordless_credentials-spa.json | 87 +++ ...device_is_shown_which_can_be_unlinked.json | 122 ++++ ...ntial_available-type=browser-response.json | 24 + ...ast_credential_available-type=browser.json | 8 + ...redential_available-type=spa-response.json | 24 + ...he_last_credential_available-type=spa.json | 8 + ...-case=one_activation_element_is_shown.json | 74 +++ ...when_passwordless_is_disabled-browser.json | 80 +++ ...ist_when_passwordless_is_disabled-spa.json | 80 +++ ...on-case=passkey_button_exists-browser.json | 138 ++++ ...ration-case=passkey_button_exists-spa.json | 138 ++++ .../fixtures/login/success/credentials.json | 18 + .../fixtures/login/success/identity.json | 13 + .../login/success/internal_context.json | 10 + .../fixtures/login/success/response.json | 11 + .../internal_context_missing_user_id.json | 7 + .../internal_context_wrong_user_id.json | 7 + .../registration/success/identity.json | 14 + .../success/internal_context.json | 7 + .../registration/success/response.json | 9 + .../fixtures/settings/success/identity.json | 14 + .../settings/success/internal_context.json | 7 + .../fixtures/settings/success/response.json | 9 + selfservice/strategy/passkey/passkey_login.go | 395 ++++++++++++ .../strategy/passkey/passkey_login_test.go | 299 +++++++++ .../strategy/passkey/passkey_registration.go | 322 ++++++++++ .../passkey/passkey_registration_test.go | 409 ++++++++++++ .../passkey/passkey_schema_extension.go | 111 ++++ .../strategy/passkey/passkey_settings.go | 419 ++++++++++++ .../strategy/passkey/passkey_settings_test.go | 451 +++++++++++++ .../strategy/passkey/passkey_strategy.go | 117 ++++ selfservice/strategy/passkey/schema.go | 15 + .../strategy/passkey/stub/login.schema.json | 31 + .../passkey/stub/login_webauthn.schema.json | 31 + .../stub/missing-identifier.schema.json | 21 + .../strategy/passkey/stub/noid.schema.json | 18 + .../strategy/passkey/stub/profile.schema.json | 25 + .../passkey/stub/registration.schema.json | 35 + .../passkey/stub/settings.schema.json | 31 + .../strategy/passkey/testfixture_test.go | 366 +++++++++++ .../strategy/password/op_registration_test.go | 1 + selfservice/strategy/password/registration.go | 2 +- .../strategy/password/registration_test.go | 4 + .../profile/.schema/registration.schema.json | 26 + selfservice/strategy/profile/strategy.go | 4 +- .../strategy/profile/two_step_registration.go | 239 +++++++ ...oad_is_set_when_identity_has_webauthn.json | 2 +- ...ebauthn_login_is_invalid-type=browser.json | 2 +- ...if_webauthn_login_is_invalid-type=spa.json | 2 +- ...swordless-case=webauthn_button_exists.json | 1 + ...passwordless_enabled=false#01-browser.json | 2 +- ...als-passwordless_enabled=false#01-spa.json | 2 +- ...passwordless_enabled=false#02-browser.json | 2 +- ...als-passwordless_enabled=false#02-spa.json | 2 +- ...ls-passwordless_enabled=false-browser.json | 2 +- ...ntials-passwordless_enabled=false-spa.json | 2 +- ...-passwordless_enabled=true#01-browser.json | 2 +- ...ials-passwordless_enabled=true#01-spa.json | 2 +- ...-passwordless_enabled=true#02-browser.json | 2 +- ...ials-passwordless_enabled=true#02-spa.json | 2 +- ...als-passwordless_enabled=true-browser.json | 2 +- ...entials-passwordless_enabled=true-spa.json | 2 +- ...device_is_shown_which_can_be_unlinked.json | 2 +- ...ntial_available-type=browser-response.json | 20 +- ...ast_credential_available-type=browser.json | 3 + ...redential_available-type=spa-response.json | 20 +- ...he_last_credential_available-type=spa.json | 3 + ...-case=one_activation_element_is_shown.json | 2 +- ...n-case=webauthn_button_exists-browser.json | 2 +- ...ation-case=webauthn_button_exists-spa.json | 2 +- .../internal_context_missing_user_id.json | 7 + .../internal_context_wrong_user_id.json | 7 + selfservice/strategy/webauthn/js/webauthn.js | 130 ---- selfservice/strategy/webauthn/login.go | 38 +- selfservice/strategy/webauthn/registration.go | 92 +-- .../strategy/webauthn/registration_test.go | 45 ++ selfservice/strategy/webauthn/settings.go | 29 +- .../strategy/webauthn/settings_test.go | 9 +- selfservice/strategy/webauthn/user.go | 42 -- spec/api.json | 87 ++- spec/swagger.json | 87 ++- test/e2e/cypress.config.ts | 1 + test/e2e/cypress/helpers/express.ts | 2 +- test/e2e/cypress/helpers/index.ts | 2 +- .../profiles/passkey/flows.spec.ts | 298 +++++++++ .../profiles/passwordless/flows.spec.ts | 29 +- .../two-steps/registration/code.spec.ts | 385 +++++++++++ .../two-steps/registration/oidc.spec.ts | 216 +++++++ .../two-steps/registration/password.spec.ts | 115 ++++ test/e2e/cypress/support/commands.ts | 11 +- test/e2e/cypress/support/index.d.ts | 11 +- test/e2e/profiles/code/.kratos.yml | 1 + test/e2e/profiles/email/.kratos.yml | 1 + test/e2e/profiles/mfa/.kratos.yml | 1 + test/e2e/profiles/mobile/.kratos.yml | 1 + test/e2e/profiles/network/.kratos.yml | 1 + test/e2e/profiles/oidc-provider/.kratos.yml | 1 + test/e2e/profiles/oidc/.kratos.yml | 1 + test/e2e/profiles/passkey/.kratos.yml | 55 ++ .../passkey/identity.traits.schema.json | 40 ++ test/e2e/profiles/passwordless/.kratos.yml | 14 + .../passwordless/identity.traits.schema.json | 8 +- test/e2e/profiles/spa/.kratos.yml | 1 + test/e2e/profiles/two-steps/.kratos.yml | 110 ++++ .../two-steps/identity.traits.schema.json | 41 ++ test/e2e/profiles/webhooks/.kratos.yml | 1 + test/e2e/run.sh | 2 +- text/id.go | 6 + text/message_login.go | 8 + text/message_registration.go | 24 + text/message_settings.go | 21 + ui/node/attributes.go | 4 + ui/node/attributes_input.go | 6 - ui/node/identifiers.go | 15 + ui/node/node.go | 1 + x/webauthnx/aaguid/aaguid.go | 55 ++ x/webauthnx/aaguid/aaguid_test.go | 51 ++ x/webauthnx/aaguid/aaguids.json | 603 ++++++++++++++++++ x/webauthnx/aaguid/passkey-aaguids.json | 105 +++ .../webauthn => x/webauthnx}/errors.go | 5 +- .../webauthn => x/webauthnx}/handler.go | 15 +- x/webauthnx/js/webauthn.js | 380 +++++++++++ .../webauthn => x/webauthnx}/nodes.go | 38 +- x/webauthnx/user.go | 47 ++ 187 files changed, 9769 insertions(+), 384 deletions(-) create mode 100644 contrib/quickstart/kratos/all-strategies/identity.schema.json create mode 100644 contrib/quickstart/kratos/all-strategies/kratos.yml create mode 100644 contrib/quickstart/kratos/passkey/identity.schema.json create mode 100644 contrib/quickstart/kratos/passkey/kratos.yml create mode 100644 internal/client-go/model_update_login_flow_with_passkey_method.go create mode 100644 internal/client-go/model_update_registration_flow_with_passkey_method.go create mode 100644 internal/client-go/model_update_settings_flow_with_passkey_method.go create mode 100644 internal/httpclient/model_update_login_flow_with_passkey_method.go create mode 100644 internal/httpclient/model_update_registration_flow_with_passkey_method.go create mode 100644 internal/httpclient/model_update_settings_flow_with_passkey_method.go create mode 100644 persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.down.sql create mode 100644 persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.up.sql create mode 100644 persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.down.sql create mode 100644 persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.up.sql create mode 100644 persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.down.sql create mode 100644 persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.up.sql create mode 100644 selfservice/hook/two_step_registration.go create mode 100644 selfservice/strategy/passkey/.schema/login.schema.json create mode 100644 selfservice/strategy/passkey/.schema/registration.schema.json create mode 100644 selfservice/strategy/passkey/.schema/settings.schema.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser-response.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa-response.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json create mode 100644 selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json create mode 100644 selfservice/strategy/passkey/fixtures/login/success/credentials.json create mode 100644 selfservice/strategy/passkey/fixtures/login/success/identity.json create mode 100644 selfservice/strategy/passkey/fixtures/login/success/internal_context.json create mode 100644 selfservice/strategy/passkey/fixtures/login/success/response.json create mode 100644 selfservice/strategy/passkey/fixtures/registration/failure/internal_context_missing_user_id.json create mode 100644 selfservice/strategy/passkey/fixtures/registration/failure/internal_context_wrong_user_id.json create mode 100644 selfservice/strategy/passkey/fixtures/registration/success/identity.json create mode 100644 selfservice/strategy/passkey/fixtures/registration/success/internal_context.json create mode 100644 selfservice/strategy/passkey/fixtures/registration/success/response.json create mode 100644 selfservice/strategy/passkey/fixtures/settings/success/identity.json create mode 100644 selfservice/strategy/passkey/fixtures/settings/success/internal_context.json create mode 100644 selfservice/strategy/passkey/fixtures/settings/success/response.json create mode 100644 selfservice/strategy/passkey/passkey_login.go create mode 100644 selfservice/strategy/passkey/passkey_login_test.go create mode 100644 selfservice/strategy/passkey/passkey_registration.go create mode 100644 selfservice/strategy/passkey/passkey_registration_test.go create mode 100644 selfservice/strategy/passkey/passkey_schema_extension.go create mode 100644 selfservice/strategy/passkey/passkey_settings.go create mode 100644 selfservice/strategy/passkey/passkey_settings_test.go create mode 100644 selfservice/strategy/passkey/passkey_strategy.go create mode 100644 selfservice/strategy/passkey/schema.go create mode 100644 selfservice/strategy/passkey/stub/login.schema.json create mode 100644 selfservice/strategy/passkey/stub/login_webauthn.schema.json create mode 100644 selfservice/strategy/passkey/stub/missing-identifier.schema.json create mode 100644 selfservice/strategy/passkey/stub/noid.schema.json create mode 100644 selfservice/strategy/passkey/stub/profile.schema.json create mode 100644 selfservice/strategy/passkey/stub/registration.schema.json create mode 100644 selfservice/strategy/passkey/stub/settings.schema.json create mode 100644 selfservice/strategy/passkey/testfixture_test.go create mode 100644 selfservice/strategy/profile/.schema/registration.schema.json create mode 100644 selfservice/strategy/profile/two_step_registration.go create mode 100644 selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_missing_user_id.json create mode 100644 selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_wrong_user_id.json delete mode 100644 selfservice/strategy/webauthn/js/webauthn.js delete mode 100644 selfservice/strategy/webauthn/user.go create mode 100644 test/e2e/cypress/integration/profiles/passkey/flows.spec.ts create mode 100644 test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts create mode 100644 test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts create mode 100644 test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts create mode 100644 test/e2e/profiles/passkey/.kratos.yml create mode 100644 test/e2e/profiles/passkey/identity.traits.schema.json create mode 100644 test/e2e/profiles/two-steps/.kratos.yml create mode 100644 test/e2e/profiles/two-steps/identity.traits.schema.json create mode 100644 x/webauthnx/aaguid/aaguid.go create mode 100644 x/webauthnx/aaguid/aaguid_test.go create mode 100644 x/webauthnx/aaguid/aaguids.json create mode 100644 x/webauthnx/aaguid/passkey-aaguids.json rename {selfservice/strategy/webauthn => x/webauthnx}/errors.go (95%) rename {selfservice/strategy/webauthn => x/webauthnx}/handler.go (74%) create mode 100644 x/webauthnx/js/webauthn.js rename {selfservice/strategy/webauthn => x/webauthnx}/nodes.go (58%) create mode 100644 x/webauthnx/user.go diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go index ef3027a936a3..5c2555eaddb2 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -72,6 +72,7 @@ func init() { "NewInfoSelfServiceSettingsUpdateUnlinkOIDC": text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC("{provider}"), "NewInfoSelfServiceRegisterWebAuthnDisplayName": text.NewInfoSelfServiceRegisterWebAuthnDisplayName(), "NewInfoSelfServiceRemoveWebAuthn": text.NewInfoSelfServiceRemoveWebAuthn("{display_name}", aSecondAgo), + "NewInfoSelfServiceRemovePasskey": text.NewInfoSelfServiceRemovePasskey("{display_name}", aSecondAgo), "NewErrorValidationVerificationFlowExpired": text.NewErrorValidationVerificationFlowExpired(aSecondAgo), "NewInfoSelfServiceVerificationSuccessful": text.NewInfoSelfServiceVerificationSuccessful(), "NewVerificationEmailSent": text.NewVerificationEmailSent(), @@ -136,6 +137,8 @@ func init() { "NewInfoRegistration": text.NewInfoRegistration(), "NewInfoRegistrationWith": text.NewInfoRegistrationWith("{provider}"), "NewInfoRegistrationContinue": text.NewInfoRegistrationContinue(), + "NewInfoRegistrationBack": text.NewInfoRegistrationBack(), + "NewInfoSelfServiceChooseCredentials": text.NewInfoSelfServiceChooseCredentials(), "NewErrorValidationRegistrationFlowExpired": text.NewErrorValidationRegistrationFlowExpired(aSecondAgo), "NewErrorValidationRecoveryFlowExpired": text.NewErrorValidationRecoveryFlowExpired(aSecondAgo), "NewRecoverySuccessful": text.NewRecoverySuccessful(inAMinute), @@ -150,9 +153,12 @@ func init() { "NewInfoNodeLoginAndLinkCredential": text.NewInfoNodeLoginAndLinkCredential(), "NewInfoNodeLabelContinue": text.NewInfoNodeLabelContinue(), "NewInfoSelfServiceSettingsRegisterWebAuthn": text.NewInfoSelfServiceSettingsRegisterWebAuthn(), + "NewInfoSelfServiceSettingsRegisterPasskey": text.NewInfoSelfServiceSettingsRegisterPasskey(), "NewInfoLoginWebAuthnPasswordless": text.NewInfoLoginWebAuthnPasswordless(), "NewInfoSelfServiceRegistrationRegisterWebAuthn": text.NewInfoSelfServiceRegistrationRegisterWebAuthn(), "NewInfoSelfServiceContinueLoginWebAuthn": text.NewInfoSelfServiceContinueLoginWebAuthn(), + "NewInfoSelfServiceLoginPasskey": text.NewInfoSelfServiceLoginPasskey(), + "NewInfoSelfServiceRegistrationRegisterPasskey": text.NewInfoSelfServiceRegistrationRegisterPasskey(), "NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(), "NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(), "NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(), diff --git a/contrib/quickstart/kratos/all-strategies/identity.schema.json b/contrib/quickstart/kratos/all-strategies/identity.schema.json new file mode 100644 index 000000000000..6915fbeff5d7 --- /dev/null +++ b/contrib/quickstart/kratos/all-strategies/identity.schema.json @@ -0,0 +1,57 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + }, + "passkey": { + "display_name": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/all-strategies/kratos.yml b/contrib/quickstart/kratos/all-strategies/kratos.yml new file mode 100644 index 000000000000..7aa978e18365 --- /dev/null +++ b/contrib/quickstart/kratos/all-strategies/kratos.yml @@ -0,0 +1,120 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +session: + whoami: + required_aal: aal1 + +selfservice: + default_browser_return_url: http://localhost:4455/ + allowed_return_urls: + - http://localhost:4455 + - http://localhost:19006/Callback + - exp://localhost:8081/--/Callback + + methods: + password: + enabled: true + webauthn: + enabled: true + config: + passwordless: true + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origin: http://localhost:4455 + passkey: + enabled: true + config: + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origins: + - http://localhost:4455 + + flows: + error: + ui_url: http://localhost:4455/error + + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 15m + required_aal: aal1 + + recovery: + enabled: true + ui_url: http://localhost:4455/recovery + use: code + + verification: + enabled: false + ui_url: http://localhost:4455/verification + use: code + after: + default_browser_return_url: http://localhost:4455/ + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + lifespan: 10m + + registration: + enable_legacy_one_step: false + lifespan: 10m + ui_url: http://localhost:4455/registration + after: + passkey: + hooks: + - hook: session + webauthn: + hooks: + - hook: session + password: + hooks: + - hook: session + - hook: show_verification_ui + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: default + schemas: + - id: default + url: file://./contrib/quickstart/kratos/all-strategies/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/contrib/quickstart/kratos/passkey/identity.schema.json b/contrib/quickstart/kratos/passkey/identity.schema.json new file mode 100644 index 000000000000..5fabe0c722eb --- /dev/null +++ b/contrib/quickstart/kratos/passkey/identity.schema.json @@ -0,0 +1,34 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/passkey/kratos.yml b/contrib/quickstart/kratos/passkey/kratos.yml new file mode 100644 index 000000000000..6be298abd92e --- /dev/null +++ b/contrib/quickstart/kratos/passkey/kratos.yml @@ -0,0 +1,107 @@ +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +session: + whoami: + required_aal: aal1 + +selfservice: + default_browser_return_url: http://localhost:4455/ + allowed_return_urls: + - http://localhost:4455 + - http://localhost:19006/Callback + - exp://example.com/Callback + - https://www.ory.sh/ + - https://example.org/ + - https://www.example.org/ + methods: + link: + config: + lifespan: 1h + code: + config: + lifespan: 1h + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + webauthn: + enabled: true + config: + passwordless: true + rp: + id: localhost + origins: + - http://localhost:4455 + display_name: Ory + passkey: + enabled: true + config: + rp: + id: localhost + origins: + - http://localhost:4455 + display_name: Ory + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + required_aal: aal1 + logout: + after: + default_browser_return_url: http://localhost:4455/login + registration: + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + webauthn: + hooks: + - hook: session + passkey: + hooks: + - hook: session + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + recovery: + ui_url: http://localhost:4455/recovery + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + schemas: + - id: default + url: file://contrib/quickstart/kratos/passkey/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/courier/message.go b/courier/message.go index 2cc8ec5a1e32..4026e9c5f46a 100644 --- a/courier/message.go +++ b/courier/message.go @@ -220,7 +220,7 @@ func (m Message) DefaultPageToken() keysetpagination.PageToken { } } -func (m Message) TableName(ctx context.Context) string { +func (m Message) TableName(context.Context) string { return "courier_messages" } diff --git a/courier/test/persistence.go b/courier/test/persistence.go index dddd8adb2cbf..3ef5529501e8 100644 --- a/courier/test/persistence.go +++ b/courier/test/persistence.go @@ -47,10 +47,14 @@ func TestPersister(ctx context.Context, newNetworkUnlessExisting NetworkWrapper, messages := make([]courier.Message, 5) t.Run("case=add messages to the queue", func(t *testing.T) { + t.Cleanup(func() { pop.SetNowFunc(time.Now) }) + now := time.Now() for k := range messages { + // We need to fake the time func to control the created_at column, which is the + // sort key for the messages. + pop.SetNowFunc(func() time.Time { return now.Add(time.Duration(k) * time.Hour) }) require.NoError(t, faker.FakeData(&messages[k])) require.NoError(t, p.AddMessage(ctx, &messages[k])) - time.Sleep(time.Second) // wait a bit so that the timestamp ordering works in MySQL. } }) diff --git a/driver/config/config.go b/driver/config/config.go index 78fa3d633854..0d755a11ba63 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -21,6 +21,7 @@ import ( "go.opentelemetry.io/otel/trace/noop" "github.com/ory/x/crdbx" + "github.com/ory/x/pointerx" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -126,6 +127,7 @@ const ( ViperKeyURLsAllowedReturnToDomains = "selfservice.allowed_return_urls" ViperKeySelfServiceRegistrationEnabled = "selfservice.flows.registration.enabled" ViperKeySelfServiceRegistrationLoginHints = "selfservice.flows.registration.login_hints" + ViperKeySelfServiceRegistrationEnableLegacyOneStep = "selfservice.flows.registration.enable_legacy_one_step" ViperKeySelfServiceRegistrationUI = "selfservice.flows.registration.ui_url" ViperKeySelfServiceRegistrationRequestLifespan = "selfservice.flows.registration.lifespan" ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after" @@ -189,6 +191,10 @@ const ( ViperKeyWebAuthnRPOrigin = "selfservice.methods.webauthn.config.rp.origin" ViperKeyWebAuthnRPOrigins = "selfservice.methods.webauthn.config.rp.origins" ViperKeyWebAuthnPasswordless = "selfservice.methods.webauthn.config.passwordless" + ViperKeyPasskeyEnabled = "selfservice.methods.passkey.enabled" + ViperKeyPasskeyRPDisplayName = "selfservice.methods.passkey.config.rp.display_name" + ViperKeyPasskeyRPID = "selfservice.methods.passkey.config.rp.id" + ViperKeyPasskeyRPOrigins = "selfservice.methods.passkey.config.rp.origins" ViperKeyOAuth2ProviderURL = "oauth2_provider.url" ViperKeyOAuth2ProviderHeader = "oauth2_provider.headers" ViperKeyOAuth2ProviderOverrideReturnTo = "oauth2_provider.override_return_to" @@ -665,6 +671,10 @@ func (p *Config) SelfServiceFlowRegistrationLoginHints(ctx context.Context) bool return p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationLoginHints) } +func (p *Config) SelfServiceFlowRegistrationTwoSteps(ctx context.Context) bool { + return !p.GetProvider(ctx).BoolF(ViperKeySelfServiceRegistrationEnableLegacyOneStep, false) +} + func (p *Config) SelfServiceFlowVerificationEnabled(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeySelfServiceVerificationEnabled) } @@ -702,7 +712,12 @@ func (p *Config) SelfServiceFlowSettingsBeforeHooks(ctx context.Context) []SelfS } func (p *Config) SelfServiceFlowRegistrationBeforeHooks(ctx context.Context) []SelfServiceHook { - return p.selfServiceHooks(ctx, ViperKeySelfServiceRegistrationBeforeHooks) + hooks := p.selfServiceHooks(ctx, ViperKeySelfServiceRegistrationBeforeHooks) + if p.SelfServiceFlowRegistrationTwoSteps(ctx) { + hooks = append(hooks, SelfServiceHook{"two_step_registration", json.RawMessage("{}")}) + } + + return hooks } func (p *Config) selfServiceHooks(ctx context.Context, key string) []SelfServiceHook { @@ -1454,6 +1469,23 @@ func (p *Config) WebAuthnConfig(ctx context.Context) *webauthn.Config { } } +func (p *Config) PasskeyConfig(ctx context.Context) *webauthn.Config { + scheme := p.SelfPublicURL(ctx).Scheme + id := p.GetProvider(ctx).String(ViperKeyPasskeyRPID) + origins := p.GetProvider(ctx).StringsF(ViperKeyPasskeyRPOrigins, []string{scheme + "://" + id}) + return &webauthn.Config{ + RPDisplayName: p.GetProvider(ctx).String(ViperKeyPasskeyRPDisplayName), + RPID: id, + RPOrigins: origins, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + AuthenticatorAttachment: "platform", + RequireResidentKey: pointerx.Ptr(true), + UserVerification: protocol.VerificationPreferred, + }, + EncodeUserIDAsString: false, + } +} + func (p *Config) HasherPasswordHashingAlgorithm(ctx context.Context) string { configValue := p.GetProvider(ctx).StringF(ViperKeyHasherAlgorithm, DefaultPasswordHashingAlgorithm) switch configValue { diff --git a/driver/config/config_test.go b/driver/config/config_test.go index ee30b48fbaa8..b2047e2fc433 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -229,11 +229,11 @@ func TestViperProvider(t *testing.T) { t.Run("hook=before", func(t *testing.T) { expHooks := []config.SelfServiceHook{ {Name: "web_hook", Config: json.RawMessage(`{"method":"GET","url":"https://test.kratos.ory.sh/before_registration_hook"}`)}, + {Name: "two_step_registration", Config: json.RawMessage(`{}`)}, } hooks := p.SelfServiceFlowRegistrationBeforeHooks(ctx) - require.Len(t, hooks, 1) assert.Equal(t, expHooks, hooks) // assert.EqualValues(t, "redirect", hook.Name) // assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config)) diff --git a/driver/registry_default.go b/driver/registry_default.go index f622d4c20336..1ab63c0af561 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -44,6 +44,7 @@ import ( "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/kratos/selfservice/strategy/lookup" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/passkey" "github.com/ory/kratos/selfservice/strategy/password" "github.com/ory/kratos/selfservice/strategy/profile" "github.com/ory/kratos/selfservice/strategy/totp" @@ -90,6 +91,7 @@ type RegistryDefault struct { hookAddressVerifier *hook.AddressVerifier hookShowVerificationUI *hook.ShowVerificationUIHook hookCodeAddressVerifier *hook.CodeAddressVerifier + hookTwoStepRegistration *hook.TwoStepRegistration identityHandler *identity.Handler identityValidator *identity.Validator @@ -318,6 +320,7 @@ func (m *RegistryDefault) selfServiceStrategies() []any { code.NewStrategy(m), link.NewStrategy(m), totp.NewStrategy(m), + passkey.NewStrategy(m), webauthn.NewStrategy(m), lookup.NewStrategy(m), } @@ -328,6 +331,9 @@ func (m *RegistryDefault) selfServiceStrategies() []any { } func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool { + if id == "profile" { + return m.Config().SelfServiceFlowRegistrationTwoSteps(ctx) + } return m.Config().SelfServiceStrategy(ctx, id).Enabled } diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index c3f809d2144e..1c436e932d4e 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -50,6 +50,13 @@ func (m *RegistryDefault) HookShowVerificationUI() *hook.ShowVerificationUIHook return m.hookShowVerificationUI } +func (m *RegistryDefault) HookTwoStepRegistration() *hook.TwoStepRegistration { + if m.hookTwoStepRegistration == nil { + m.hookTwoStepRegistration = hook.NewTwoStepRegistration(m) + } + return m.hookTwoStepRegistration +} + func (m *RegistryDefault) WithHooks(hooks map[string]func(config.SelfServiceHook) interface{}) { m.injectedSelfserviceHooks = hooks } @@ -67,6 +74,8 @@ func (m *RegistryDefault) getHooks(credentialsType string, configs []config.Self i = append(i, m.HookAddressVerifier()) case hook.KeyVerificationUI: i = append(i, m.HookShowVerificationUI()) + case hook.KeyTwoStepRegistration: + i = append(i, m.HookTwoStepRegistration()) default: var found bool for name, m := range m.injectedSelfserviceHooks { diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 0dd43e2efc70..d1c02490dcb5 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -205,9 +205,13 @@ func TestDriverDefault_Hooks(t *testing.T) { expect func(reg *driver.RegistryDefault) []registration.PreHookExecutor }{ { - uc: "No hooks configured", - prep: func(conf *config.Config) {}, - expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return nil }, + uc: "No hooks configured", + prep: func(conf *config.Config) {}, + expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { + return []registration.PreHookExecutor{ + hook.NewTwoStepRegistration(reg), + } + }, }, { uc: "Two web_hooks are configured", @@ -221,6 +225,7 @@ func TestDriverDefault_Hooks(t *testing.T) { return []registration.PreHookExecutor{ hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewTwoStepRegistration(reg), } }, }, @@ -620,7 +625,7 @@ func TestDriverDefault_Strategies(t *testing.T) { ctx := context.Background() t.Run("case=registration", func(t *testing.T) { t.Parallel() - for k, tc := range []struct { + for _, tc := range []struct { name string prep func(conf *config.Config) expect []string @@ -631,6 +636,7 @@ func TestDriverDefault_Strategies(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) }, + expect: []string{"profile"}, }, { name: "only password", @@ -638,7 +644,7 @@ func TestDriverDefault_Strategies(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) }, - expect: []string{"password"}, + expect: []string{"password", "profile"}, }, { name: "oidc and password", @@ -647,7 +653,7 @@ func TestDriverDefault_Strategies(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) }, - expect: []string{"password", "oidc"}, + expect: []string{"password", "oidc", "profile"}, }, { name: "oidc, password and totp", @@ -657,7 +663,7 @@ func TestDriverDefault_Strategies(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) }, - expect: []string{"password", "oidc"}, + expect: []string{"password", "oidc", "profile"}, }, { name: "password and code", @@ -665,10 +671,10 @@ func TestDriverDefault_Strategies(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) }, - expect: []string{"password", "code"}, + expect: []string{"password", "profile", "code"}, }, } { - t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { + t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { conf, reg := internal.NewVeryFastRegistryWithoutDB(t) tc.prep(conf) @@ -880,7 +886,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("case=all login strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "code", "totp", "webauthn", "lookup_secret"} + expects := []string{"password", "oidc", "code", "totp", "passkey", "webauthn", "lookup_secret"} s := reg.AllLoginStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -889,7 +895,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { }) t.Run("case=all registration strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "code", "webauthn"} + expects := []string{"password", "oidc", "profile", "code", "passkey", "webauthn"} s := reg.AllRegistrationStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -898,7 +904,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { }) t.Run("case=all settings strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "profile", "totp", "webauthn", "lookup_secret"} + expects := []string{"password", "oidc", "profile", "totp", "passkey", "webauthn", "lookup_secret"} s := reg.AllSettingsStrategies() require.Len(t, s, len(expects)) for k, e := range expects { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 6a355e055639..c7a7be169224 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -828,6 +828,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, "lookup_secret": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, @@ -861,6 +864,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterOIDCLoginMethod" }, @@ -945,6 +951,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, @@ -1230,6 +1239,12 @@ }, "after": { "$ref": "#/definitions/selfServiceAfterRegistration" + }, + "enable_legacy_one_step": { + "type": "boolean", + "title": "Disable two-step registration", + "description": "Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To revert to one-step registration, set this to `true`.", + "default": false } } }, @@ -1689,6 +1704,67 @@ "required": ["config"] } }, + "passkey": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables the Passkey method", + "default": false + }, + "config": { + "type": "object", + "title": "Passkey Configuration", + "properties": { + "rp": { + "title": "Relying Party (RP) Config", + "properties": { + "display_name": { + "type": "string", + "title": "Relying Party Display Name", + "description": "A name to help the user identify this RP.", + "examples": ["Ory Foundation"] + }, + "id": { + "type": "string", + "title": "Relying Party Identifier", + "description": "The id must be a subset of the domain currently in the browser.", + "examples": ["ory.sh"] + }, + "origins": { + "type": "array", + "title": "Relying Party Origins", + "description": "A list of explicit RP origins. If left empty, this defaults to either `origin` or `id`, prepended with the current protocol schema (HTTP or HTTPS).", + "items": { + "type": "string", + "format": "uri", + "examples": [ + "https://www.ory.sh", + "https://auth.ory.sh" + ] + } + } + }, + "type": "object", + "required": ["display_name", "id"] + } + }, + "additionalProperties": false + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + }, + "then": { + "required": ["config"] + } + }, "oidc": { "type": "object", "title": "Specify OpenID Connect and OAuth2 Configuration", diff --git a/embedx/identity_extension.schema.json b/embedx/identity_extension.schema.json index 6ad7765ef839..88c07b5f1153 100644 --- a/embedx/identity_extension.schema.json +++ b/embedx/identity_extension.schema.json @@ -30,6 +30,15 @@ } } }, + "passkey": { + "type": "object", + "additionalProperties": false, + "properties": { + "display_name": { + "type": "boolean" + } + } + }, "totp": { "type": "object", "additionalProperties": false, diff --git a/identity/credentials.go b/identity/credentials.go index c18b9df97f5d..3cc910c5a74e 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -86,6 +86,8 @@ const ( CredentialsTypeLookup CredentialsType = "lookup_secret" CredentialsTypeWebAuthn CredentialsType = "webauthn" CredentialsTypeCodeAuth CredentialsType = "code" + CredentialsTypePasskey CredentialsType = "passkey" + CredentialsTypeProfile CredentialsType = "profile" ) func (c CredentialsType) String() string { @@ -106,6 +108,8 @@ func (c CredentialsType) ToUiNodeGroup() node.UiNodeGroup { return node.LookupGroup case CredentialsTypeCodeAuth: return node.CodeGroup + case CredentialsTypePasskey: + return node.PasskeyGroup default: return node.DefaultGroup } @@ -118,6 +122,7 @@ var AllCredentialTypes = []CredentialsType{ CredentialsTypeLookup, CredentialsTypeWebAuthn, CredentialsTypeCodeAuth, + CredentialsTypePasskey, } const ( @@ -138,6 +143,7 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { CredentialsTypeCodeAuth, CredentialsTypeRecoveryLink, CredentialsTypeRecoveryCode, + CredentialsTypePasskey, } { if t.String() == in { return t, true diff --git a/identity/credentials_webauthn.go b/identity/credentials_webauthn.go index ecfd132ea0b1..0816046d23e4 100644 --- a/identity/credentials_webauthn.go +++ b/identity/credentials_webauthn.go @@ -7,9 +7,11 @@ import ( "time" "github.com/go-webauthn/webauthn/webauthn" + + "github.com/ory/kratos/x/webauthnx/aaguid" ) -// CredentialsConfig is the struct that is being used as part of the identity credentials. +// CredentialsWebAuthnConfig is the struct that is being used as part of the identity credentials. type CredentialsWebAuthnConfig struct { // List of webauthn credentials. Credentials CredentialsWebAuthn `json:"credentials"` @@ -19,17 +21,24 @@ type CredentialsWebAuthnConfig struct { type CredentialsWebAuthn []CredentialWebAuthn func CredentialFromWebAuthn(credential *webauthn.Credential, isPasswordless bool) *CredentialWebAuthn { - return &CredentialWebAuthn{ + cred := &CredentialWebAuthn{ ID: credential.ID, PublicKey: credential.PublicKey, IsPasswordless: isPasswordless, AttestationType: credential.AttestationType, + AddedAt: time.Now().UTC().Round(time.Second), Authenticator: AuthenticatorWebAuthn{ AAGUID: credential.Authenticator.AAGUID, SignCount: credential.Authenticator.SignCount, CloneWarning: credential.Authenticator.CloneWarning, }, } + id := aaguid.Lookup(credential.Authenticator.AAGUID) + if id != nil { + cred.DisplayName = id.Name + } + + return cred } func (c CredentialsWebAuthn) ToWebAuthn() (result []webauthn.Credential) { @@ -39,15 +48,26 @@ func (c CredentialsWebAuthn) ToWebAuthn() (result []webauthn.Credential) { return result } +// PasswordlessOnly returns only passwordless credentials. +func (c CredentialsWebAuthn) PasswordlessOnly() (result []webauthn.Credential) { + for k, cc := range c { + if cc.IsPasswordless { + result = append(result, *c[k].ToWebAuthn()) + } + } + return result +} + +// ToWebAuthnFiltered returns only the appropriate credentials for the requested +// AAL. For AAL1, only passwordless credentials are returned, for AAL2, only +// non-passwordless credentials are returned. func (c CredentialsWebAuthn) ToWebAuthnFiltered(aal AuthenticatorAssuranceLevel) (result []webauthn.Credential) { for k, cc := range c { - if aal == AuthenticatorAssuranceLevel1 && !cc.IsPasswordless { - continue - } else if aal == AuthenticatorAssuranceLevel2 && cc.IsPasswordless { - continue + if (aal == AuthenticatorAssuranceLevel1 && cc.IsPasswordless) || + (aal == AuthenticatorAssuranceLevel2 && !cc.IsPasswordless) { + result = append(result, *c[k].ToWebAuthn()) } - result = append(result, *c[k].ToWebAuthn()) } return result } diff --git a/identity/credentials_webauthn_test.go b/identity/credentials_webauthn_test.go index c081c1826c41..ed3dc9689a7b 100644 --- a/identity/credentials_webauthn_test.go +++ b/identity/credentials_webauthn_test.go @@ -6,7 +6,10 @@ package identity import ( "testing" + "github.com/stretchr/testify/require" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" ) @@ -41,4 +44,21 @@ func TestCredentialConversion(t *testing.T) { assert.True(t, fromWebAuthn.IsPasswordless) fromWebAuthn = CredentialFromWebAuthn(expected, false) assert.False(t, fromWebAuthn.IsPasswordless) + + expected.Authenticator.AAGUID = uuid.Must(uuid.FromString("ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4")).Bytes() + fromWebAuthn = CredentialFromWebAuthn(expected, false) + assert.Equal(t, "Google Password Manager", fromWebAuthn.DisplayName) +} + +func TestPasswordlessOnly(t *testing.T) { + a := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("a")}, false) + b := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("b")}, false) + c := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("c")}, true) + d := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("d")}, false) + e := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("e")}, true) + expected := CredentialsWebAuthn{a, b, c, d, e} + + actual := expected.PasswordlessOnly() + require.Len(t, actual, 2) + assert.Equal(t, []webauthn.Credential{*c.ToWebAuthn(), *e.ToWebAuthn()}, actual) } diff --git a/identity/identity.go b/identity/identity.go index 63839a8fad05..55dd66155ea9 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -177,7 +177,7 @@ func (t *Traits) UnmarshalJSON(data []byte) error { return nil } -func (i Identity) TableName(ctx context.Context) string { +func (i Identity) TableName(context.Context) string { return "identities" } @@ -230,20 +230,34 @@ func (i *Identity) DeleteCredentialsType(t CredentialsType) { delete(i.Credentials, t) } -func (i *Identity) GetCredentialsOr(t CredentialsType, or *Credentials) *Credentials { +// GetCredentialsOr returns the credentials for a given CredentialsType. If the +// credentials do not exist, the fallback is returned. +func (i *Identity) GetCredentialsOr(t CredentialsType, fallback *Credentials) *Credentials { c, ok := i.GetCredentials(t) if !ok { - return or + return fallback } return c } -func (i *Identity) UpsertCredentialsConfig(t CredentialsType, conf []byte, version int) { +type CredentialsOptions func(c *Credentials) + +func WithAdditionalIdentifier(identifier string) CredentialsOptions { + return func(c *Credentials) { + c.Identifiers = append(c.Identifiers, identifier) + } +} + +func (i *Identity) UpsertCredentialsConfig(t CredentialsType, conf []byte, version int, opt ...CredentialsOptions) { c, ok := i.GetCredentials(t) if !ok { c = &Credentials{} } + for _, optionFn := range opt { + optionFn(c) + } + c.Type = t c.IdentityID = i.ID c.Config = conf diff --git a/identity/pool.go b/identity/pool.go index 89eaf9927637..3ea7d4129f6f 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -94,7 +94,10 @@ type ( // InjectTraitsSchemaURL sets the identity's traits JSON schema URL from the schema's ID. InjectTraitsSchemaURL(ctx context.Context, i *Identity) error - // FindIdentityByAnyCaseSensitiveCredentialIdentifier returns an identity by matching the identifier to any of the identity's credentials. + // FindIdentityByCredentialIdentifier returns an identity by matching the identifier to any of the identity's credentials. FindIdentityByCredentialIdentifier(ctx context.Context, identifier string, caseSensitive bool) (*Identity, error) + + // FindIdentityByWebauthnUserHandle returns an identity matching a webauthn user handle. + FindIdentityByWebauthnUserHandle(ctx context.Context, userHandle []byte) (*Identity, error) } ) diff --git a/identity/test/pool.go b/identity/test/pool.go index c351c57c6160..450b5c1ea881 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -243,15 +243,6 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, return i } - webAuthnIdentity := func(schemaID string, credentialsID string) *identity.Identity { - i := identity.NewIdentity(schemaID) - i.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ - Type: identity.CredentialsTypeWebAuthn, Identifiers: []string{credentialsID}, - Config: sqlxx.JSONRawMessage(`{"credentials":[{}]}`), - }) - return i - } - oidcIdentity := func(schemaID string, credentialsID string) *identity.Identity { i := identity.NewIdentity(schemaID) i.SetCredentials(identity.CredentialsTypeOIDC, identity.Credentials{ @@ -376,7 +367,14 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, }) t.Run("case=run migrations when fetching credentials", func(t *testing.T) { - expected := webAuthnIdentity(altSchema.ID, "webauthn") + expected := func(schemaID string, credentialsID string) *identity.Identity { + i := identity.NewIdentity(schemaID) + i.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, Identifiers: []string{credentialsID}, + Config: sqlxx.JSONRawMessage(`{"credentials":[{}]}`), + }) + return i + }(altSchema.ID, "webauthn") require.NoError(t, p.CreateIdentity(ctx, expected)) createdIDs = append(createdIDs, expected.ID) @@ -857,6 +855,49 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, }) }) + t.Run("case=find identity by its webauthn credential user handle", func(t *testing.T) { + expected := identity.NewIdentity("") + expected.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{"find-webauth-user-handle-identifier@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{ + "credentials": [ + { + "added_at": "2024-02-13T10:36:16Z", + "attestation_type": "none", + "authenticator": { + "aaguid": "+/wwBxVOTsyMC24CBVfXvQ==", + "clone_warning": false, + "sign_count": 0 + }, + "display_name": "Yubikey", + "id": "f2uGd/Bg1rGcGXtYp4MT4WcN+eA=", + "is_passwordless": true, + "public_key": "pQECAyYgASFYIBkNvUxvjdhuA36FworTmS/rxZR1I+NyRWBpoTYY/R+CIlggw+gFFrFoEi+rS82zq7+tDHAukBUJcFpQ7Z3NLBZH5vk=" + } + ], + "user_handle": "51z80nYJTSGmr6UBe1VGLg==" +}`), + }) + expected.Traits = identity.Traits(`{}`) + userHandle := x.Must(base64.StdEncoding.DecodeString("51z80nYJTSGmr6UBe1VGLg==")) + + require.NoError(t, p.CreateIdentity(ctx, expected)) + createdIDs = append(createdIDs, expected.ID) + + actual, err := p.FindIdentityByWebauthnUserHandle(ctx, userHandle) + require.NoError(t, err) + + expected.Credentials = nil + assertEqual(t, expected, actual) + + t.Run("not if on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, err = p.FindIdentityByWebauthnUserHandle(ctx, userHandle) + require.ErrorIs(t, err, sqlcon.ErrNoRows) + }) + }) + t.Run("case=find identity only by credentials identifier", func(t *testing.T) { expected := passwordIdentity("", "find-credentials-identifier-only@ory.sh") expected.Traits = identity.Traits(`{}`) diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index 0f14f1ccb470..4085540e8053 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -101,6 +101,7 @@ docs/UpdateLoginFlowBody.md docs/UpdateLoginFlowWithCodeMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md +docs/UpdateLoginFlowWithPasskeyMethod.md docs/UpdateLoginFlowWithPasswordMethod.md docs/UpdateLoginFlowWithTotpMethod.md docs/UpdateLoginFlowWithWebAuthnMethod.md @@ -110,11 +111,13 @@ docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md +docs/UpdateRegistrationFlowWithPasskeyMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md docs/UpdateSettingsFlowBody.md docs/UpdateSettingsFlowWithLookupMethod.md docs/UpdateSettingsFlowWithOidcMethod.md +docs/UpdateSettingsFlowWithPasskeyMethod.md docs/UpdateSettingsFlowWithPasswordMethod.md docs/UpdateSettingsFlowWithProfileMethod.md docs/UpdateSettingsFlowWithTotpMethod.md @@ -217,6 +220,7 @@ model_update_login_flow_body.go model_update_login_flow_with_code_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go +model_update_login_flow_with_passkey_method.go model_update_login_flow_with_password_method.go model_update_login_flow_with_totp_method.go model_update_login_flow_with_web_authn_method.go @@ -226,11 +230,13 @@ model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go +model_update_registration_flow_with_passkey_method.go model_update_registration_flow_with_password_method.go model_update_registration_flow_with_web_authn_method.go model_update_settings_flow_body.go model_update_settings_flow_with_lookup_method.go model_update_settings_flow_with_oidc_method.go +model_update_settings_flow_with_passkey_method.go model_update_settings_flow_with_password_method.go model_update_settings_flow_with_profile_method.go model_update_settings_flow_with_totp_method.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index 7f5de7166ef1..345c54129d58 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -224,6 +224,7 @@ Class | Method | HTTP request | Description - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) + - [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) @@ -233,11 +234,13 @@ Class | Method | HTTP request | Description - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) + - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) - [UpdateSettingsFlowWithOidcMethod](docs/UpdateSettingsFlowWithOidcMethod.md) + - [UpdateSettingsFlowWithPasskeyMethod](docs/UpdateSettingsFlowWithPasskeyMethod.md) - [UpdateSettingsFlowWithPasswordMethod](docs/UpdateSettingsFlowWithPasswordMethod.md) - [UpdateSettingsFlowWithProfileMethod](docs/UpdateSettingsFlowWithProfileMethod.md) - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_ui_node.go b/internal/client-go/model_ui_node.go index ca88879a7414..e73f3c5e37d8 100644 --- a/internal/client-go/model_ui_node.go +++ b/internal/client-go/model_ui_node.go @@ -18,7 +18,7 @@ import ( // UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `` tag, or an `` but also `some plain text`. type UiNode struct { Attributes UiNodeAttributes `json:"attributes"` - // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup + // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup Group string `json:"group"` Messages []UiText `json:"messages"` Meta UiNodeMeta `json:"meta"` diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go index 285456f2af98..fbf7e0f1b04e 100644 --- a/internal/client-go/model_ui_node_input_attributes.go +++ b/internal/client-go/model_ui_node_input_attributes.go @@ -28,6 +28,8 @@ type UiNodeInputAttributes struct { NodeType string `json:"node_type"` // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Onclick *string `json:"onclick,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. + Onload *string `json:"onload,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -227,6 +229,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnload returns the Onload field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnload() string { + if o == nil || o.Onload == nil { + var ret string + return ret + } + return *o.Onload +} + +// GetOnloadOk returns a tuple with the Onload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadOk() (*string, bool) { + if o == nil || o.Onload == nil { + return nil, false + } + return o.Onload, true +} + +// HasOnload returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnload() bool { + if o != nil && o.Onload != nil { + return true + } + + return false +} + +// SetOnload gets a reference to the given string and assigns it to the Onload field. +func (o *UiNodeInputAttributes) SetOnload(v string) { + o.Onload = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -368,6 +402,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.Onload != nil { + toSerialize["onload"] = o.Onload + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/internal/client-go/model_update_login_flow_with_passkey_method.go b/internal/client-go/model_update_login_flow_with_passkey_method.go new file mode 100644 index 000000000000..90bbcd6ddf1c --- /dev/null +++ b/internal/client-go/model_update_login_flow_with_passkey_method.go @@ -0,0 +1,182 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithPasskeyMethod Update Login Flow with Passkey Method +type UpdateLoginFlowWithPasskeyMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method should be set to \"passkey\" when logging in using the Passkey strategy. + Method string `json:"method"` + // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyLogin *string `json:"passkey_login,omitempty"` +} + +// NewUpdateLoginFlowWithPasskeyMethod instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithPasskeyMethod(method string) *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithPasskeyMethodWithDefaults instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithPasskeyMethodWithDefaults() *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyLogin returns the PasskeyLogin field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLogin() string { + if o == nil || o.PasskeyLogin == nil { + var ret string + return ret + } + return *o.PasskeyLogin +} + +// GetPasskeyLoginOk returns a tuple with the PasskeyLogin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLoginOk() (*string, bool) { + if o == nil || o.PasskeyLogin == nil { + return nil, false + } + return o.PasskeyLogin, true +} + +// HasPasskeyLogin returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasPasskeyLogin() bool { + if o != nil && o.PasskeyLogin != nil { + return true + } + + return false +} + +// SetPasskeyLogin gets a reference to the given string and assigns it to the PasskeyLogin field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetPasskeyLogin(v string) { + o.PasskeyLogin = &v +} + +func (o UpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyLogin != nil { + toSerialize["passkey_login"] = o.PasskeyLogin + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithPasskeyMethod struct { + value *UpdateLoginFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) Get() *UpdateLoginFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Set(val *UpdateLoginFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithPasskeyMethod(val *UpdateLoginFlowWithPasskeyMethod) *NullableUpdateLoginFlowWithPasskeyMethod { + return &NullableUpdateLoginFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_registration_flow_with_passkey_method.go b/internal/client-go/model_update_registration_flow_with_passkey_method.go new file mode 100644 index 000000000000..38d59713262e --- /dev/null +++ b/internal/client-go/model_update_registration_flow_with_passkey_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithPasskeyMethod Update Registration Flow with Passkey Method +type UpdateRegistrationFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a Passkey. + Method string `json:"method"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeyRegister *string `json:"passkey_register,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithPasskeyMethod instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithPasskeyMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults() *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRegister returns the PasskeyRegister field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegister() string { + if o == nil || o.PasskeyRegister == nil { + var ret string + return ret + } + return *o.PasskeyRegister +} + +// GetPasskeyRegisterOk returns a tuple with the PasskeyRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegisterOk() (*string, bool) { + if o == nil || o.PasskeyRegister == nil { + return nil, false + } + return o.PasskeyRegister, true +} + +// HasPasskeyRegister returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasPasskeyRegister() bool { + if o != nil && o.PasskeyRegister != nil { + return true + } + + return false +} + +// SetPasskeyRegister gets a reference to the given string and assigns it to the PasskeyRegister field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetPasskeyRegister(v string) { + o.PasskeyRegister = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRegister != nil { + toSerialize["passkey_register"] = o.PasskeyRegister + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithPasskeyMethod struct { + value *UpdateRegistrationFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) Get() *UpdateRegistrationFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Set(val *UpdateRegistrationFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithPasskeyMethod(val *UpdateRegistrationFlowWithPasskeyMethod) *NullableUpdateRegistrationFlowWithPasskeyMethod { + return &NullableUpdateRegistrationFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_settings_flow_with_passkey_method.go b/internal/client-go/model_update_settings_flow_with_passkey_method.go new file mode 100644 index 000000000000..c7103432afcd --- /dev/null +++ b/internal/client-go/model_update_settings_flow_with_passkey_method.go @@ -0,0 +1,219 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateSettingsFlowWithPasskeyMethod Update Settings Flow with Passkey Method +type UpdateSettingsFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing. + Method string `json:"method"` + // Remove a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyRemove *string `json:"passkey_remove,omitempty"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeySettingsRegister *string `json:"passkey_settings_register,omitempty"` +} + +// NewUpdateSettingsFlowWithPasskeyMethod instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateSettingsFlowWithPasskeyMethod(method string) *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithPasskeyMethodWithDefaults instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateSettingsFlowWithPasskeyMethodWithDefaults() *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRemove returns the PasskeyRemove field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemove() string { + if o == nil || o.PasskeyRemove == nil { + var ret string + return ret + } + return *o.PasskeyRemove +} + +// GetPasskeyRemoveOk returns a tuple with the PasskeyRemove field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemoveOk() (*string, bool) { + if o == nil || o.PasskeyRemove == nil { + return nil, false + } + return o.PasskeyRemove, true +} + +// HasPasskeyRemove returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeyRemove() bool { + if o != nil && o.PasskeyRemove != nil { + return true + } + + return false +} + +// SetPasskeyRemove gets a reference to the given string and assigns it to the PasskeyRemove field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeyRemove(v string) { + o.PasskeyRemove = &v +} + +// GetPasskeySettingsRegister returns the PasskeySettingsRegister field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegister() string { + if o == nil || o.PasskeySettingsRegister == nil { + var ret string + return ret + } + return *o.PasskeySettingsRegister +} + +// GetPasskeySettingsRegisterOk returns a tuple with the PasskeySettingsRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegisterOk() (*string, bool) { + if o == nil || o.PasskeySettingsRegister == nil { + return nil, false + } + return o.PasskeySettingsRegister, true +} + +// HasPasskeySettingsRegister returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeySettingsRegister() bool { + if o != nil && o.PasskeySettingsRegister != nil { + return true + } + + return false +} + +// SetPasskeySettingsRegister gets a reference to the given string and assigns it to the PasskeySettingsRegister field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeySettingsRegister(v string) { + o.PasskeySettingsRegister = &v +} + +func (o UpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRemove != nil { + toSerialize["passkey_remove"] = o.PasskeyRemove + } + if o.PasskeySettingsRegister != nil { + toSerialize["passkey_settings_register"] = o.PasskeySettingsRegister + } + return json.Marshal(toSerialize) +} + +type NullableUpdateSettingsFlowWithPasskeyMethod struct { + value *UpdateSettingsFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) Get() *UpdateSettingsFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Set(val *UpdateSettingsFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithPasskeyMethod(val *UpdateSettingsFlowWithPasskeyMethod) *NullableUpdateSettingsFlowWithPasskeyMethod { + return &NullableUpdateSettingsFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index 0f14f1ccb470..4085540e8053 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -101,6 +101,7 @@ docs/UpdateLoginFlowBody.md docs/UpdateLoginFlowWithCodeMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md +docs/UpdateLoginFlowWithPasskeyMethod.md docs/UpdateLoginFlowWithPasswordMethod.md docs/UpdateLoginFlowWithTotpMethod.md docs/UpdateLoginFlowWithWebAuthnMethod.md @@ -110,11 +111,13 @@ docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md +docs/UpdateRegistrationFlowWithPasskeyMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md docs/UpdateSettingsFlowBody.md docs/UpdateSettingsFlowWithLookupMethod.md docs/UpdateSettingsFlowWithOidcMethod.md +docs/UpdateSettingsFlowWithPasskeyMethod.md docs/UpdateSettingsFlowWithPasswordMethod.md docs/UpdateSettingsFlowWithProfileMethod.md docs/UpdateSettingsFlowWithTotpMethod.md @@ -217,6 +220,7 @@ model_update_login_flow_body.go model_update_login_flow_with_code_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go +model_update_login_flow_with_passkey_method.go model_update_login_flow_with_password_method.go model_update_login_flow_with_totp_method.go model_update_login_flow_with_web_authn_method.go @@ -226,11 +230,13 @@ model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go +model_update_registration_flow_with_passkey_method.go model_update_registration_flow_with_password_method.go model_update_registration_flow_with_web_authn_method.go model_update_settings_flow_body.go model_update_settings_flow_with_lookup_method.go model_update_settings_flow_with_oidc_method.go +model_update_settings_flow_with_passkey_method.go model_update_settings_flow_with_password_method.go model_update_settings_flow_with_profile_method.go model_update_settings_flow_with_totp_method.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 7f5de7166ef1..345c54129d58 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -224,6 +224,7 @@ Class | Method | HTTP request | Description - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) + - [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) @@ -233,11 +234,13 @@ Class | Method | HTTP request | Description - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) + - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) - [UpdateSettingsFlowWithOidcMethod](docs/UpdateSettingsFlowWithOidcMethod.md) + - [UpdateSettingsFlowWithPasskeyMethod](docs/UpdateSettingsFlowWithPasskeyMethod.md) - [UpdateSettingsFlowWithPasswordMethod](docs/UpdateSettingsFlowWithPasswordMethod.md) - [UpdateSettingsFlowWithProfileMethod](docs/UpdateSettingsFlowWithProfileMethod.md) - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) diff --git a/internal/httpclient/model_ui_node.go b/internal/httpclient/model_ui_node.go index ca88879a7414..e73f3c5e37d8 100644 --- a/internal/httpclient/model_ui_node.go +++ b/internal/httpclient/model_ui_node.go @@ -18,7 +18,7 @@ import ( // UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `` tag, or an `` but also `some plain text`. type UiNode struct { Attributes UiNodeAttributes `json:"attributes"` - // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup + // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup Group string `json:"group"` Messages []UiText `json:"messages"` Meta UiNodeMeta `json:"meta"` diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go index 285456f2af98..fbf7e0f1b04e 100644 --- a/internal/httpclient/model_ui_node_input_attributes.go +++ b/internal/httpclient/model_ui_node_input_attributes.go @@ -28,6 +28,8 @@ type UiNodeInputAttributes struct { NodeType string `json:"node_type"` // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Onclick *string `json:"onclick,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. + Onload *string `json:"onload,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -227,6 +229,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnload returns the Onload field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnload() string { + if o == nil || o.Onload == nil { + var ret string + return ret + } + return *o.Onload +} + +// GetOnloadOk returns a tuple with the Onload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadOk() (*string, bool) { + if o == nil || o.Onload == nil { + return nil, false + } + return o.Onload, true +} + +// HasOnload returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnload() bool { + if o != nil && o.Onload != nil { + return true + } + + return false +} + +// SetOnload gets a reference to the given string and assigns it to the Onload field. +func (o *UiNodeInputAttributes) SetOnload(v string) { + o.Onload = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -368,6 +402,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.Onload != nil { + toSerialize["onload"] = o.Onload + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/internal/httpclient/model_update_login_flow_with_passkey_method.go b/internal/httpclient/model_update_login_flow_with_passkey_method.go new file mode 100644 index 000000000000..90bbcd6ddf1c --- /dev/null +++ b/internal/httpclient/model_update_login_flow_with_passkey_method.go @@ -0,0 +1,182 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithPasskeyMethod Update Login Flow with Passkey Method +type UpdateLoginFlowWithPasskeyMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method should be set to \"passkey\" when logging in using the Passkey strategy. + Method string `json:"method"` + // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyLogin *string `json:"passkey_login,omitempty"` +} + +// NewUpdateLoginFlowWithPasskeyMethod instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithPasskeyMethod(method string) *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithPasskeyMethodWithDefaults instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithPasskeyMethodWithDefaults() *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyLogin returns the PasskeyLogin field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLogin() string { + if o == nil || o.PasskeyLogin == nil { + var ret string + return ret + } + return *o.PasskeyLogin +} + +// GetPasskeyLoginOk returns a tuple with the PasskeyLogin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLoginOk() (*string, bool) { + if o == nil || o.PasskeyLogin == nil { + return nil, false + } + return o.PasskeyLogin, true +} + +// HasPasskeyLogin returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasPasskeyLogin() bool { + if o != nil && o.PasskeyLogin != nil { + return true + } + + return false +} + +// SetPasskeyLogin gets a reference to the given string and assigns it to the PasskeyLogin field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetPasskeyLogin(v string) { + o.PasskeyLogin = &v +} + +func (o UpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyLogin != nil { + toSerialize["passkey_login"] = o.PasskeyLogin + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithPasskeyMethod struct { + value *UpdateLoginFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) Get() *UpdateLoginFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Set(val *UpdateLoginFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithPasskeyMethod(val *UpdateLoginFlowWithPasskeyMethod) *NullableUpdateLoginFlowWithPasskeyMethod { + return &NullableUpdateLoginFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_registration_flow_with_passkey_method.go b/internal/httpclient/model_update_registration_flow_with_passkey_method.go new file mode 100644 index 000000000000..38d59713262e --- /dev/null +++ b/internal/httpclient/model_update_registration_flow_with_passkey_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithPasskeyMethod Update Registration Flow with Passkey Method +type UpdateRegistrationFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a Passkey. + Method string `json:"method"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeyRegister *string `json:"passkey_register,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithPasskeyMethod instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithPasskeyMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults() *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRegister returns the PasskeyRegister field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegister() string { + if o == nil || o.PasskeyRegister == nil { + var ret string + return ret + } + return *o.PasskeyRegister +} + +// GetPasskeyRegisterOk returns a tuple with the PasskeyRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegisterOk() (*string, bool) { + if o == nil || o.PasskeyRegister == nil { + return nil, false + } + return o.PasskeyRegister, true +} + +// HasPasskeyRegister returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasPasskeyRegister() bool { + if o != nil && o.PasskeyRegister != nil { + return true + } + + return false +} + +// SetPasskeyRegister gets a reference to the given string and assigns it to the PasskeyRegister field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetPasskeyRegister(v string) { + o.PasskeyRegister = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRegister != nil { + toSerialize["passkey_register"] = o.PasskeyRegister + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithPasskeyMethod struct { + value *UpdateRegistrationFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) Get() *UpdateRegistrationFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Set(val *UpdateRegistrationFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithPasskeyMethod(val *UpdateRegistrationFlowWithPasskeyMethod) *NullableUpdateRegistrationFlowWithPasskeyMethod { + return &NullableUpdateRegistrationFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_settings_flow_with_passkey_method.go b/internal/httpclient/model_update_settings_flow_with_passkey_method.go new file mode 100644 index 000000000000..c7103432afcd --- /dev/null +++ b/internal/httpclient/model_update_settings_flow_with_passkey_method.go @@ -0,0 +1,219 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateSettingsFlowWithPasskeyMethod Update Settings Flow with Passkey Method +type UpdateSettingsFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing. + Method string `json:"method"` + // Remove a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyRemove *string `json:"passkey_remove,omitempty"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeySettingsRegister *string `json:"passkey_settings_register,omitempty"` +} + +// NewUpdateSettingsFlowWithPasskeyMethod instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateSettingsFlowWithPasskeyMethod(method string) *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithPasskeyMethodWithDefaults instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateSettingsFlowWithPasskeyMethodWithDefaults() *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRemove returns the PasskeyRemove field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemove() string { + if o == nil || o.PasskeyRemove == nil { + var ret string + return ret + } + return *o.PasskeyRemove +} + +// GetPasskeyRemoveOk returns a tuple with the PasskeyRemove field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemoveOk() (*string, bool) { + if o == nil || o.PasskeyRemove == nil { + return nil, false + } + return o.PasskeyRemove, true +} + +// HasPasskeyRemove returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeyRemove() bool { + if o != nil && o.PasskeyRemove != nil { + return true + } + + return false +} + +// SetPasskeyRemove gets a reference to the given string and assigns it to the PasskeyRemove field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeyRemove(v string) { + o.PasskeyRemove = &v +} + +// GetPasskeySettingsRegister returns the PasskeySettingsRegister field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegister() string { + if o == nil || o.PasskeySettingsRegister == nil { + var ret string + return ret + } + return *o.PasskeySettingsRegister +} + +// GetPasskeySettingsRegisterOk returns a tuple with the PasskeySettingsRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegisterOk() (*string, bool) { + if o == nil || o.PasskeySettingsRegister == nil { + return nil, false + } + return o.PasskeySettingsRegister, true +} + +// HasPasskeySettingsRegister returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeySettingsRegister() bool { + if o != nil && o.PasskeySettingsRegister != nil { + return true + } + + return false +} + +// SetPasskeySettingsRegister gets a reference to the given string and assigns it to the PasskeySettingsRegister field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeySettingsRegister(v string) { + o.PasskeySettingsRegister = &v +} + +func (o UpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRemove != nil { + toSerialize["passkey_remove"] = o.PasskeyRemove + } + if o.PasskeySettingsRegister != nil { + toSerialize["passkey_settings_register"] = o.PasskeySettingsRegister + } + return json.Marshal(toSerialize) +} + +type NullableUpdateSettingsFlowWithPasskeyMethod struct { + value *UpdateSettingsFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) Get() *UpdateSettingsFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Set(val *UpdateSettingsFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithPasskeyMethod(val *UpdateSettingsFlowWithPasskeyMethod) *NullableUpdateSettingsFlowWithPasskeyMethod { + return &NullableUpdateSettingsFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/registrationhelpers/helpers.go b/internal/registrationhelpers/helpers.go index 67a636b567fb..9fbf7f08211d 100644 --- a/internal/registrationhelpers/helpers.go +++ b/internal/registrationhelpers/helpers.go @@ -278,6 +278,7 @@ func AssertRegistrationRespectsValidation(t *testing.T, reg *driver.RegistryDefa func AssertCommonErrorCases(t *testing.T, flows []string) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) testhelpers.SetDefaultIdentitySchemaFromRaw(conf, basicSchema) uiTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) diff --git a/internal/testhelpers/selfservice_settings.go b/internal/testhelpers/selfservice_settings.go index 37a0e97ac472..cf3cdad15866 100644 --- a/internal/testhelpers/selfservice_settings.go +++ b/internal/testhelpers/selfservice_settings.go @@ -67,8 +67,7 @@ func InitializeSettingsFlowViaBrowser(t *testing.T, client *http.Client, isSPA b require.NoError(t, res.Body.Close()) - rs, res, err := publicClient.FrontendApi.GetSettingsFlow(context.Background()). - Id(flowID).Execute() + rs, res, err := publicClient.FrontendApi.GetSettingsFlow(context.Background()).Id(flowID).Execute() require.NoError(t, err, "%s", ioutilx.MustReadAll(res.Body)) assert.Empty(t, rs.Active) diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go index adbc4f81a392..29e8997f3438 100644 --- a/internal/testhelpers/session.go +++ b/internal/testhelpers/session.go @@ -28,7 +28,7 @@ type SessionLifespanProvider struct { e time.Duration } -func (p *SessionLifespanProvider) SessionLifespan(ctx context.Context) time.Duration { +func (p *SessionLifespanProvider) SessionLifespan(context.Context) time.Duration { return p.e } diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index a22b62827f8a..cdbd46427d38 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -6,6 +6,7 @@ package identity import ( "context" "database/sql" + "encoding/base64" "fmt" "sort" "strings" @@ -241,6 +242,44 @@ func (p *IdentityPersister) FindByCredentialsIdentifier(ctx context.Context, ct return i.CopyWithoutCredentials(), creds, nil } +func (p *IdentityPersister) FindIdentityByWebauthnUserHandle(ctx context.Context, userHandle []byte) (_ *identity.Identity, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindIdentityByWebauthnUserHandle") + defer otelx.End(span, &err) + + var id identity.Identity + + var jsonPath string + switch p.GetConnection(ctx).Dialect.Name() { + case "sqlite", "mysql": + jsonPath = "$.user_handle" + default: + jsonPath = "user_handle" + } + + if err := p.GetConnection(ctx).RawQuery(fmt.Sprintf(` +SELECT identities.* +FROM identities +INNER JOIN identity_credentials + ON identities.id = identity_credentials.identity_id + AND identities.nid = identity_credentials.nid + AND identity_credentials.identity_credential_type_id = ( + SELECT id + FROM identity_credential_types + WHERE name = ? + ) +WHERE identity_credentials.config ->> '%s' = ? + AND identities.nid = ? +LIMIT 1`, jsonPath), + identity.CredentialsTypeWebAuthn, + base64.StdEncoding.EncodeToString(userHandle), + p.NetworkID(ctx), + ).First(&id); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &id, nil +} + var credentialsTypes = struct { sync.RWMutex m map[identity.CredentialsType]*identity.CredentialsTypeTable @@ -315,11 +354,11 @@ func (p *IdentityPersister) createIdentityCredentials(ctx context.Context, conn } for _, cred := range credentials { - for _, ids := range cred.Identifiers { + for _, identifier := range cred.Identifiers { // Force case-insensitivity and trimming for identifiers - ids = NormalizeIdentifier(cred.Type, ids) + identifier = NormalizeIdentifier(cred.Type, identifier) - if ids == "" { + if identifier == "" { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf( "Unable to create identity credentials with missing or empty identifier.")) } @@ -330,7 +369,7 @@ func (p *IdentityPersister) createIdentityCredentials(ctx context.Context, conn } identifiers = append(identifiers, &identity.CredentialIdentifier{ - Identifier: ids, + Identifier: identifier, IdentityCredentialsID: cred.ID, IdentityCredentialsTypeID: ct.ID, NID: p.NetworkID(ctx), diff --git a/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.down.sql b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.down.sql new file mode 100644 index 000000000000..656e0e5de5fe --- /dev/null +++ b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'passkey'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.up.sql b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.up.sql new file mode 100644 index 000000000000..7d5e3f0faa1a --- /dev/null +++ b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.up.sql @@ -0,0 +1,3 @@ +INSERT INTO identity_credential_types (id, name) +SELECT '8d0ca544-9bf6-45d3-bd75-0bbb3aeba3c7', 'passkey' +WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'passkey'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.down.sql new file mode 100644 index 000000000000..c6e7f9d2939e --- /dev/null +++ b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credentials_user_handle_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.up.sql new file mode 100644 index 000000000000..14c295e29c5d --- /dev/null +++ b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.up.sql @@ -0,0 +1,3 @@ +CREATE INVERTED INDEX identity_credentials_user_handle_idx + ON identity_credentials (config) + WHERE config ->> 'user_handle' IS NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.down.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.up.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/schema/extension.go b/schema/extension.go index c217f409c8c3..62db865d1f3f 100644 --- a/schema/extension.go +++ b/schema/extension.go @@ -12,10 +12,11 @@ import ( "github.com/ory/jsonschema/v3" "github.com/ory/kratos/embedx" + "github.com/ory/x/jsonschemax" ) const ( - extensionName string = "ory.sh/kratos" + ExtensionName string = "ory.sh/kratos" ) type ( @@ -27,6 +28,9 @@ type ( WebAuthn struct { Identifier bool `json:"identifier"` } `json:"webauthn"` + Passkey struct { + DisplayName bool `json:"display_name"` + } `json:"passkey"` TOTP struct { AccountName bool `json:"account_name"` } `json:"totp"` @@ -64,6 +68,16 @@ type ( ExtensionRunnerOption func(*ExtensionRunner) ) +func (e *ExtensionConfig) EnhancePath(path jsonschemax.Path) map[string]any { + props := path.CustomProperties + if props == nil { + props = make(map[string]any) + } + props[ExtensionName] = e + + return props +} + func WithValidateRunners(runners ...ValidateExtension) ExtensionRunnerOption { return func(r *ExtensionRunner) { r.validateRunners = append(r.validateRunners, runners...) @@ -91,7 +105,7 @@ func NewExtensionRunner(ctx context.Context, opts ...ExtensionRunnerOption) (*Ex } r.compile = func(ctx jsonschema.CompilerContext, m map[string]interface{}) (interface{}, error) { - if raw, ok := m[extensionName]; ok { + if raw, ok := m[ExtensionName]; ok { var b bytes.Buffer if err := json.NewEncoder(&b).Encode(raw); err != nil { return nil, errors.WithStack(err) @@ -136,7 +150,7 @@ func NewExtensionRunner(ctx context.Context, opts ...ExtensionRunnerOption) (*Ex } func (r *ExtensionRunner) Register(compiler *jsonschema.Compiler) *ExtensionRunner { - compiler.Extensions[extensionName] = r.Extension() + compiler.Extensions[ExtensionName] = r.Extension() return r } diff --git a/selfservice/flow/login/extension_identifier_label.go b/selfservice/flow/login/extension_identifier_label.go index 9d28dd8ecf9f..961a7c5d7324 100644 --- a/selfservice/flow/login/extension_identifier_label.go +++ b/selfservice/flow/login/extension_identifier_label.go @@ -67,6 +67,7 @@ func GetIdentifierLabelFromSchemaWithField(ctx context.Context, schemaURL string func (i *identifierLabelExtension) Run(_ jsonschema.CompilerContext, config schema.ExtensionConfig, rawSchema map[string]interface{}) error { if config.Credentials.Password.Identifier || config.Credentials.WebAuthn.Identifier || + config.Credentials.Passkey.DisplayName || config.Credentials.TOTP.AccountName || config.Credentials.Code.Identifier { if title, ok := rawSchema["title"]; ok { diff --git a/selfservice/flow/login/extension_identifier_label_test.go b/selfservice/flow/login/extension_identifier_label_test.go index 3b3a21400fb1..9d2bbc80e667 100644 --- a/selfservice/flow/login/extension_identifier_label_test.go +++ b/selfservice/flow/login/extension_identifier_label_test.go @@ -102,6 +102,13 @@ func TestGetIdentifierLabelFromSchema(t *testing.T) { }, expected: text.NewInfoNodeLabelGenerated("Email"), }, + { + name: "email for passkey", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Passkey.DisplayName = true + }, + expected: text.NewInfoNodeLabelGenerated("Email"), + }, { name: "email for all", emailConfig: func(c *schema.ExtensionConfig) { diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index 37044f7ca2d8..a6bb0d55d60b 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -218,6 +218,8 @@ func (f Flow) GetID() uuid.UUID { return f.ID } +// IsForced returns true if the login flow was triggered to re-authenticate the user. +// This is the case if the refresh query parameter is set to true. func (f *Flow) IsForced() bool { return f.Refresh } diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go index 9f1a144ffc2d..c9d9145d46e3 100644 --- a/selfservice/flow/login/sort.go +++ b/selfservice/flow/login/sort.go @@ -15,6 +15,7 @@ func sortNodes(ctx context.Context, n node.Nodes) error { node.OpenIDConnectGroup, node.DefaultGroup, node.WebAuthnGroup, + node.PasskeyGroup, node.CodeGroup, node.PasswordGroup, node.TOTPGroup, diff --git a/selfservice/flow/registration/sort.go b/selfservice/flow/registration/sort.go index 15348dd56512..f68dbb79ca59 100644 --- a/selfservice/flow/registration/sort.go +++ b/selfservice/flow/registration/sort.go @@ -13,11 +13,13 @@ func SortNodes(ctx context.Context, n node.Nodes, schemaRef string) error { return n.SortBySchema(ctx, node.SortBySchema(schemaRef), node.SortByGroups([]node.UiNodeGroup{ - node.DefaultGroup, node.OpenIDConnectGroup, + node.DefaultGroup, node.WebAuthnGroup, + node.PasskeyGroup, node.CodeGroup, node.PasswordGroup, + node.ProfileGroup, }), node.SortUpdateOrder(node.PasswordLoginOrder), node.SortUseOrderAppend([]string{ diff --git a/selfservice/hook/hooks.go b/selfservice/hook/hooks.go index 3eabac19d036..3b2060c9c6a1 100644 --- a/selfservice/hook/hooks.go +++ b/selfservice/hook/hooks.go @@ -4,9 +4,10 @@ package hook const ( - KeySessionIssuer = "session" - KeySessionDestroyer = "revoke_active_sessions" - KeyWebHook = "web_hook" - KeyAddressVerifier = "require_verified_address" - KeyVerificationUI = "show_verification_ui" + KeySessionIssuer = "session" + KeySessionDestroyer = "revoke_active_sessions" + KeyWebHook = "web_hook" + KeyAddressVerifier = "require_verified_address" + KeyVerificationUI = "show_verification_ui" + KeyTwoStepRegistration = "two_step_registration" ) diff --git a/selfservice/hook/two_step_registration.go b/selfservice/hook/two_step_registration.go new file mode 100644 index 000000000000..def83d5cc7e7 --- /dev/null +++ b/selfservice/hook/two_step_registration.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "net/http" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +var _ registration.PreHookExecutor = new(TwoStepRegistration) + +type ( + twoStepRegistrationDeps interface { + x.WriterProvider + config.Provider + } + + TwoStepRegistration struct { + d twoStepRegistrationDeps + } +) + +func NewTwoStepRegistration(d twoStepRegistrationDeps) *TwoStepRegistration { + return &TwoStepRegistration{d: d} +} + +func (e *TwoStepRegistration) ExecuteRegistrationPreHook(_ http.ResponseWriter, _ *http.Request, regFlow *registration.Flow) (err error) { + stepOneNodes := make([]*node.Node, 0, len(regFlow.UI.Nodes)) + stepTwoNodes := make([]*node.Node, 0, len(regFlow.UI.Nodes)) + for _, n := range regFlow.UI.Nodes { + if n.Group == node.ProfileGroup || n.Group == node.OpenIDConnectGroup || n.Group == node.DefaultGroup { + stepOneNodes = append(stepOneNodes, n) + } else { + stepTwoNodes = append(stepTwoNodes, n) + } + } + + regFlow.UI.Nodes = stepOneNodes + + regFlow.InternalContext, err = sjson.SetBytes(regFlow.InternalContext, "stepTwoNodes", stepTwoNodes) + if err != nil { + return errors.WithStack(err) + } + regFlow.InternalContext, err = sjson.SetBytes(regFlow.InternalContext, "stepOneNodes", stepOneNodes) + if err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/selfservice/strategy/code/code_registration.go b/selfservice/strategy/code/code_registration.go index d8691b54b224..4093480fb91e 100644 --- a/selfservice/strategy/code/code_registration.go +++ b/selfservice/strategy/code/code_registration.go @@ -60,7 +60,7 @@ type RegistrationCode struct { NID uuid.UUID `json:"-" faker:"-" db:"nid"` } -func (RegistrationCode) TableName(ctx context.Context) string { +func (RegistrationCode) TableName(context.Context) string { return "identity_registration_codes" } diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index a4b52e980754..fd8993447744 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -275,6 +275,7 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error var message *text.Message var resendNode *node.Node + var backNode *node.Node switch f.GetFlowName() { case flow.RecoveryFlow: @@ -284,6 +285,7 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error resendNode = node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeResendOTP()) + case flow.VerificationFlow: route = verification.RouteSubmitFlow codeMetaLabel = text.NewInfoNodeLabelVerificationCode() @@ -342,6 +344,18 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeResendOTP()) + + // Insert a back button if we have a two-step registration screen, so that the + // user can navigate back to the credential selection screen. + if s.deps.Config().SelfServiceFlowRegistrationTwoSteps(ctx) { + backNode = node.NewInputField( + "screen", + "credential-selection", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistrationBack()) + } + default: return errors.WithStack(herodot.ErrBadRequest.WithReason("received an unexpected flow type")) } @@ -365,6 +379,10 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error freshNodes.Append(resendNode) } + if backNode != nil { + freshNodes.Append(backNode) + } + f.GetUI().Nodes = freshNodes f.GetUI().Method = "POST" diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index f9da6d29fcb5..dca3054e0c8e 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -93,7 +93,7 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, rf *registration. type options func(*identity.Identity) error -func WithCredentials(via identity.CodeAddressType, usedAt sql.NullTime) options { +func withCredentials(via identity.CodeAddressType, usedAt sql.NullTime) options { return func(i *identity.Identity) error { return i.SetCredentialsWithConfig(identity.CredentialsTypeCodeAuth, identity.Credentials{Type: identity.CredentialsTypePassword, Identifiers: []string{}}, &identity.CredentialsCode{AddressType: via, UsedAt: usedAt}) } @@ -268,7 +268,7 @@ func (s *Strategy) registrationVerifyCode(ctx context.Context, f *registration.F } // Step 4: The code was correct, populate the Identity credentials and traits - if err := s.handleIdentityTraits(ctx, f, p.Traits, p.TransientPayload, i, WithCredentials(registrationCode.AddressType, registrationCode.UsedAt)); err != nil { + if err := s.handleIdentityTraits(ctx, f, p.Traits, p.TransientPayload, i, withCredentials(registrationCode.AddressType, registrationCode.UsedAt)); err != nil { return errors.WithStack(err) } diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index 1ca150b5f0ba..27c645a94190 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -5,6 +5,7 @@ package code_test import ( "context" + _ "embed" "encoding/json" "fmt" "io" @@ -14,8 +15,6 @@ import ( "strings" "testing" - _ "embed" - "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" @@ -47,6 +46,7 @@ func TestRegistrationCodeStrategyDisabled(t *testing.T) { conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypePassword.String()), false) conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), false) + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) @@ -103,6 +103,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ {"hook": "session"}, }) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index ea4d4364691e..0e579906a94f 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -71,7 +71,7 @@ type Configuration struct { // SubjectSource is a flag which controls from which endpoint the subject identifier is taken by microsoft provider. // Can be either `userinfo` or `me`. - // If the value is `uerinfo` then the subject identifier is taken from sub field of uderifo standard endpoint response. + // If the value is `userinfo` then the subject identifier is taken from sub field of userinfo standard endpoint response. // If the value is `me` then the `id` field of https://graph.microsoft.com/v1.0/me response is taken as subject. // The default is `userinfo`. SubjectSource string `json:"subject_source"` diff --git a/selfservice/strategy/passkey/.schema/login.schema.json b/selfservice/strategy/passkey/.schema/login.schema.json new file mode 100644 index 000000000000..af01cb95d323 --- /dev/null +++ b/selfservice/strategy/passkey/.schema/login.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/passkey/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "passkey_login": { + "type": "string" + }, + "method": { + "type": "string" + } + } +} diff --git a/selfservice/strategy/passkey/.schema/registration.schema.json b/selfservice/strategy/passkey/.schema/registration.schema.json new file mode 100644 index 000000000000..a2ac0441c0d0 --- /dev/null +++ b/selfservice/strategy/passkey/.schema/registration.schema.json @@ -0,0 +1,23 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/passkey/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "traits": { + "description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect." + }, + "method": { + "type": "string" + }, + "passkey_register": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/passkey/.schema/settings.schema.json b/selfservice/strategy/passkey/.schema/settings.schema.json new file mode 100644 index 000000000000..7753e441d04f --- /dev/null +++ b/selfservice/strategy/passkey/.schema/settings.schema.json @@ -0,0 +1,20 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/passkey/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "const": "passkey" + }, + "passkey_settings_register": { + "type": "string" + }, + "passkey_remove": { + "type": "string" + } + } +} + diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json new file mode 100644 index 000000000000..d2dd6567d240 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -0,0 +1,96 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "username webauthn", + "disabled": false, + "name": "identifier", + "node_type": "input", + "required": true, + "type": "text", + "value": "" + }, + "group": "default", + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "onclick": "window.__oryPasskeyLogin()", + "onload": "window.__oryPasskeyLoginAutocompleteInit()", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_challenge", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json new file mode 100644 index 000000000000..c331d4f4280f --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -0,0 +1,87 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "foo@bar.com" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "onclick": "window.__oryPasskeyLogin()", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_challenge", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json new file mode 100644 index 000000000000..c331d4f4280f --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -0,0 +1,87 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "foo@bar.com" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "onclick": "window.__oryPasskeyLogin()", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_challenge", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json new file mode 100644 index 000000000000..f9032e39049d --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -0,0 +1,122 @@ +[ + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.__oryPasskeySettingsRegistration()", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1050019, + "text": "Add passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_remove", + "node_type": "input", + "type": "submit", + "value": "626172626172" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "bar" + }, + "id": 1050020, + "text": "Remove passkey \"bar\"", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_remove", + "node_type": "input", + "type": "submit", + "value": "666f6f666f6f" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + }, + "id": 1050020, + "text": "Remove passkey \"foo\"", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_settings_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser-response.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser-response.json new file mode 100644 index 000000000000..fa4420351e94 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser-response.json @@ -0,0 +1,24 @@ +{ + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_remove", + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050020, + "text": "Remove passkey \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser.json new file mode 100644 index 000000000000..cccc13f86841 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser.json @@ -0,0 +1,8 @@ +{ + "passkey_register_trigger": [ + "" + ], + "passkey_remove": [ + "666f6f666f6f" + ] +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa-response.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa-response.json new file mode 100644 index 000000000000..fa4420351e94 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa-response.json @@ -0,0 +1,24 @@ +{ + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_remove", + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050020, + "text": "Remove passkey \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa.json new file mode 100644 index 000000000000..cccc13f86841 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa.json @@ -0,0 +1,8 @@ +{ + "passkey_register_trigger": [ + "" + ], + "passkey_remove": [ + "666f6f666f6f" + ] +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json new file mode 100644 index 000000000000..7e5c5b3d082b --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -0,0 +1,74 @@ +[ + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.__oryPasskeySettingsRegistration()", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1050019, + "text": "Add passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_settings_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json new file mode 100644 index 000000000000..cd42a6256ce0 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json @@ -0,0 +1,80 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json new file mode 100644 index 000000000000..cd42a6256ce0 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json @@ -0,0 +1,80 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json new file mode 100644 index 000000000000..18e0cda77811 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -0,0 +1,138 @@ +[ + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.__oryPasskeyRegistration()", + "type": "button" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json new file mode 100644 index 000000000000..18e0cda77811 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -0,0 +1,138 @@ +[ + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.__oryPasskeyRegistration()", + "type": "button" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/fixtures/login/success/credentials.json b/selfservice/strategy/passkey/fixtures/login/success/credentials.json new file mode 100644 index 000000000000..1fdf7ca53fb8 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/credentials.json @@ -0,0 +1,18 @@ +{ + "credentials": [ + { + "id": "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==", + "public_key": "pQECAyYgASFYIPW2FsD6d/Lc7SU33hMhJUxafOA3JWpsLka8eKO+OPRkIlggkkPt8ocrupQOuvy+8HbQLSLiu899EdchJlWdMPE1tiw=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 3, + "clone_warning": false + }, + "display_name": "some-key", + "added_at": "2021-08-17T10:18:55Z", + "is_passwordless": true + } + ], + "user_handle": "c29tZS1yYW5kb20tdXNlci1oYW5kbGU=" +} diff --git a/selfservice/strategy/passkey/fixtures/login/success/identity.json b/selfservice/strategy/passkey/fixtures/login/success/identity.json new file mode 100644 index 000000000000..2a6ad6975599 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/identity.json @@ -0,0 +1,13 @@ +{ + "id": "f5d1b6a3-a4bb-44f7-9161-f4f877efe9ad", + "schema_id": "default", + "schema_url": "http://localhost:4455/schemas/default", + "state": "active", + "state_changed_at": "2021-08-17T12:18:01.690425+02:00", + "traits": { + "email": "foo@bar.com", + "website": "https://www.ory.sh" + }, + "created_at": "2021-08-17T12:18:01.690741+02:00", + "updated_at": "2021-08-17T12:18:01.690741+02:00" +} diff --git a/selfservice/strategy/passkey/fixtures/login/success/internal_context.json b/selfservice/strategy/passkey/fixtures/login/success/internal_context.json new file mode 100644 index 000000000000..fcbca8822781 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/internal_context.json @@ -0,0 +1,10 @@ +{ + "passkey_session_data": { + "challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM", + "user_id": "", + "allowed_credentials": [ + "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==" + ], + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/login/success/response.json b/selfservice/strategy/passkey/fixtures/login/success/response.json new file mode 100644 index 000000000000..3a4ff0ad5ac2 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/response.json @@ -0,0 +1,11 @@ +{ + "id": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ", + "rawId": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ", + "type": "public-key", + "response": { + "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAACg", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV3paQ1dVTG1hcTV4VEFsQTBZWUhscW91YnFBaGUxQVdkTFJaQ0lCQU1jTSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", + "signature": "MEQCIHtRzzmLJrTPucNIRpPkstxR8oGJEzrm558LFe2jHTesAiAy2SGuBMDkdVdMJU4WJR2qFSpbAHQUvwG--Gv3vK8vDA", + "userHandle": "c29tZS1yYW5kb20tdXNlci1oYW5kbGU=" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_missing_user_id.json b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_missing_user_id.json new file mode 100644 index 000000000000..2e83f5ae0a8a --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_missing_user_id.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_wrong_user_id.json b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_wrong_user_id.json new file mode 100644 index 000000000000..50d686c1ed30 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_wrong_user_id.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "wrong", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/success/identity.json b/selfservice/strategy/passkey/fixtures/registration/success/identity.json new file mode 100644 index 000000000000..5aa6d11cae1c --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/identity.json @@ -0,0 +1,14 @@ +{ + "id": "6e11a9a7-62fd-4c88-871a-097f18f0306f", + "schema_id": "default", + "schema_url": "http://localhost:4455/schemas/default", + "state": "active", + "state_changed_at": "2021-08-17T11:15:59.232051+02:00", + "traits": { + "email": "foo@bar.com", + "website": "https://www.ory.sh" + }, + "created_at": "2021-08-17T11:15:59.232288+02:00", + "updated_at": "2021-08-17T11:15:59.232288+02:00" +} + diff --git a/selfservice/strategy/passkey/fixtures/registration/success/internal_context.json b/selfservice/strategy/passkey/fixtures/registration/success/internal_context.json new file mode 100644 index 000000000000..c4c2e93e7b8e --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/internal_context.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "bhGpp2L9TIiHGgl/GPAwbw==", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/success/response.json b/selfservice/strategy/passkey/fixtures/registration/success/response.json new file mode 100644 index 000000000000..7d2b3819927d --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/response.json @@ -0,0 +1,9 @@ +{ + "id": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "rawId": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "type": "public-key", + "response": { + "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAABAAAAAAAAAAAAAAAAAAAAAAAQC9cjq8R8uS6u9pQD2oWlnZdKvYLF0c1dgSeMSZpE1R__LrlSlPhVWyZVUcGBszzXNSIwllGACtB9GEqXeYc_j6lAQIDJiABIVggFFzdor6hBMgrpYLCds8Uu2JtPaaaxKU6LEAUT6QRZ5UiWCA24TI4vED6rrTUjykchoAln67u5GT1nwmzjvrk79HhlQ", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVWx4SFNUa3VNdnRWRG9WOXk1bGh1OU95TlVQOFA3TVAwUllBVDZJbV9yWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0" + } +} diff --git a/selfservice/strategy/passkey/fixtures/settings/success/identity.json b/selfservice/strategy/passkey/fixtures/settings/success/identity.json new file mode 100644 index 000000000000..5aa6d11cae1c --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/settings/success/identity.json @@ -0,0 +1,14 @@ +{ + "id": "6e11a9a7-62fd-4c88-871a-097f18f0306f", + "schema_id": "default", + "schema_url": "http://localhost:4455/schemas/default", + "state": "active", + "state_changed_at": "2021-08-17T11:15:59.232051+02:00", + "traits": { + "email": "foo@bar.com", + "website": "https://www.ory.sh" + }, + "created_at": "2021-08-17T11:15:59.232288+02:00", + "updated_at": "2021-08-17T11:15:59.232288+02:00" +} + diff --git a/selfservice/strategy/passkey/fixtures/settings/success/internal_context.json b/selfservice/strategy/passkey/fixtures/settings/success/internal_context.json new file mode 100644 index 000000000000..c4c2e93e7b8e --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/settings/success/internal_context.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "bhGpp2L9TIiHGgl/GPAwbw==", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/settings/success/response.json b/selfservice/strategy/passkey/fixtures/settings/success/response.json new file mode 100644 index 000000000000..7d2b3819927d --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/settings/success/response.json @@ -0,0 +1,9 @@ +{ + "id": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "rawId": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "type": "public-key", + "response": { + "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAABAAAAAAAAAAAAAAAAAAAAAAAQC9cjq8R8uS6u9pQD2oWlnZdKvYLF0c1dgSeMSZpE1R__LrlSlPhVWyZVUcGBszzXNSIwllGACtB9GEqXeYc_j6lAQIDJiABIVggFFzdor6hBMgrpYLCds8Uu2JtPaaaxKU6LEAUT6QRZ5UiWCA24TI4vED6rrTUjykchoAln67u5GT1nwmzjvrk79HhlQ", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVWx4SFNUa3VNdnRWRG9WOXk1bGh1OU95TlVQOFA3TVAwUllBVDZJbV9yWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0" + } +} diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go new file mode 100644 index 000000000000..63d5ec66f2f0 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_login.go @@ -0,0 +1,395 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + _ "embed" + "encoding/json" + "net/http" + "strings" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flowhelpers" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/x/decoderx" +) + +func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { + webauthnx.RegisterWebauthnRoute(r) +} + +func (s *Strategy) PopulateLoginMethod(r *http.Request, aal identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser || aal != identity.AuthenticatorAssuranceLevel1 { + return nil + } + + return s.populateLoginMethodForPasskeys(r, sr) +} + +func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *login.Flow) error { + if loginFlow.IsForced() { + return s.populateLoginMethodForRefresh(r, loginFlow) + } + + ctx := r.Context() + + loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginDiscoverableLogin() + if err != nil { + return errors.WithStack(err) + } + + loginFlow.InternalContext, err = sjson.SetBytes( + loginFlow.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + loginFlow.UI.Nodes.Upsert(node.NewInputField( + "identifier", + "", + node.DefaultGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute, + func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, + ).WithMetaLabel(identifierLabel)) + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + loginFlow.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + }}) + + loginFlow.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *login.Flow) error { + ctx := r.Context() + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, loginFlow, s.ID()) + if identifier == "" { + return nil + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + if err != nil { + return err + } + + cred, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no passkey + return nil + } + + var conf identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return errors.WithStack(err) + } + + webAuthCreds := conf.Credentials.ToWebAuthn() + if len(webAuthCreds) == 0 { + // Identity has no webauthn + return nil + } + + passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ + Name: passkeyIdentifier, + ID: conf.UserHandle, + Credentials: webAuthCreds, + Config: webAuthn.Config, + }) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) + } + + loginFlow.InternalContext, err = sjson.SetBytes( + loginFlow.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + loginFlow.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + }}) + + loginFlow.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + loginFlow.UI.SetNode(node.NewInputField( + "identifier", + passkeyIdentifier, + node.DefaultGroup, + node.InputAttributeTypeHidden, + )) + + return nil +} + +func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, err error) error { + if f != nil { + f.UI.Nodes.ResetNodes(node.PasskeyLogin) + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +// Update Login Flow with Passkey Method +// +// swagger:model updateLoginFlowWithPasskeyMethod +type updateLoginFlowWithPasskeyMethod struct { + // Method should be set to "passkey" when logging in using the Passkey strategy. + // + // required: true + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // Login a WebAuthn Security Key + // + // This must contain the ID of the WebAuthN connection. + Login string `json:"passkey_login"` +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) { + if f.Type != flow.TypeBrowser { + return nil, flow.ErrStrategyNotResponsible + } + + var p updateLoginFlowWithPasskeyMethod + if err := s.hd.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if len(p.Login) > 0 || p.Method == s.SettingsStrategyID() { + // This method has only two submit buttons + p.Method = s.SettingsStrategyID() + } else { + return nil, flow.ErrStrategyNotResponsible + } + + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + return s.loginPasswordless(w, r, f, &p) +} + +func (s *Strategy) loginPasswordless(w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithPasskeyMethod) (i *identity.Identity, err error) { + if err = login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if err = flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if len(p.Login) == 0 { + // Reset all nodes to not confuse users. + f.UI.Nodes = node.Nodes{} + + if err = s.populateLoginMethodForPasskeys(r, f); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + redirectTo := f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String() + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(redirectTo)) + } else { + http.Redirect(w, r, redirectTo, http.StatusSeeOther) + } + + return nil, errors.WithStack(flow.ErrCompletedByStrategy) + } + + return s.loginAuthenticate(w, r, f, p, identity.AuthenticatorAssuranceLevel1) +} + +func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithPasskeyMethod, _ identity.AuthenticatorAssuranceLevel) (*identity.Identity, error) { + ctx := r.Context() + + web, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) + } + + webAuthnResponse, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(p.Login)) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response.").WithDebug(err.Error()))) + } + + var webAuthnSess webauthn.SessionData + if err := json.Unmarshal([]byte(gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)).Raw), &webAuthnSess); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) + } + webAuthnSess.UserID = nil + + userHandle := webAuthnResponse.Response.UserHandle + credentialType := identity.CredentialsTypePasskey + i, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePasskey, string(userHandle)) + if err != nil { + // Migration strategy: Don't give up yet! If we don't find a "passkey" credential + // here, look for a "webauthn" credential next + if i, err = s.d.PrivilegedIdentityPool().FindIdentityByWebauthnUserHandle(ctx, userHandle); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnCredentials())) + } + credentialType = identity.CredentialsTypeWebAuthn + } + err = s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + WithReason("Could not load identity credentials"). + WithWrap(err))) + } + + c, ok := i.GetCredentials(credentialType) + if !ok { + return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnRegistered())) + } + + var o identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(c.Config, &o); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + WithReason("The WebAuthn credentials could not be decoded properly"). + WithDebug(err.Error()). + WithWrap(err))) + } + + webAuthCreds := o.Credentials.PasswordlessOnly() + + _, err = web.ValidateDiscoverableLogin( + func(rawID, userHandle []byte) (user webauthn.User, err error) { + return webauthnx.NewUser(userHandle, webAuthCreds, web.Config), nil + }, webAuthnSess, webAuthnResponse) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/"))) + } + + // Remove the WebAuthn URL from the internal context now that it is set! + f.InternalContext, err = sjson.DeleteBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(err)) + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + } + + return i, nil +} diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go new file mode 100644 index 000000000000..cae6aa0ee4dd --- /dev/null +++ b/selfservice/strategy/passkey/passkey_login_test.go @@ -0,0 +1,299 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + _ "embed" + "encoding/json" + "net/http" + "net/url" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/passkey" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/snapshotx" +) + +var ( + //go:embed fixtures/login/success/identity.json + loginSuccessIdentity []byte + //go:embed fixtures/login/success/credentials.json + loginPasswordlessCredentials []byte + //go:embed fixtures/login/success/internal_context.json + loginPasswordlessContext []byte + //go:embed fixtures/login/success/response.json + loginPasswordlessResponse []byte +) + +func TestPopulateLoginMethod(t *testing.T) { + t.Parallel() + fix := newLoginFixture(t) + s := passkey.NewStrategy(fix.reg) + + t.Run("case=should not handle AAL2", func(t *testing.T) { + loginFlow := &login.Flow{Type: flow.TypeBrowser} + assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel2, loginFlow)) + }) + + t.Run("case=should not handle API flows", func(t *testing.T) { + loginFlow := &login.Flow{Type: flow.TypeAPI} + assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel1, loginFlow)) + }) +} + +func TestCompleteLogin(t *testing.T) { + t.Parallel() + fix := newLoginFixture(t) + + t.Run("case=should return webauthn.js", func(t *testing.T) { + res, err := fix.publicTS.Client().Get(fix.publicTS.URL + "/.well-known/ory/webauthn.js") + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "text/javascript; charset=UTF-8", res.Header.Get("Content-Type")) + }) + + t.Run("flow=passwordless", func(t *testing.T) { + t.Run("case=passkey button exists", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, fix.publicTS, false, true, false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "2.attributes.nonce", + "2.attributes.src", + "5.attributes.value", + }) + }) + + t.Run("case=passkey shows error if user tries to sign in but no such user exists", func(t *testing.T) { + payload := func(v url.Values) { + v.Set("method", "passkey") + v.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + fix.checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, text.NewErrorValidationSuchNoWebAuthnUser().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, false, payload, testhelpers.NewClientWithCookies(t)) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, true, payload, testhelpers.NewClientWithCookies(t)) + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if passkey login is invalid", func(t *testing.T) { + payload := func(v url.Values) { + v.Set("method", "passkey") + v.Set("passkey_login", "invalid passkey data") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + fix.checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, "Unable to parse WebAuthn response.", gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, false, payload, testhelpers.NewClientWithCookies(t)) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, true, payload, testhelpers.NewClientWithCookies(t)) + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if passkey login is empty", func(t *testing.T) { + payload := func(v url.Values) { + v.Set("method", "passkey") + } + + t.Run("type=browser", func(t *testing.T) { + _, res := fix.loginViaBrowser(t, false, payload, testhelpers.NewClientWithCookies(t)) + fix.checkURL(t, true, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, true, payload, testhelpers.NewClientWithCookies(t)) + fix.checkURL(t, false, res) + assert.Equal(t, "browser_location_change_required", gjson.Get(body, "error.id").String(), "%s", body) + }) + }) + + t.Run("case=fails with invalid internal state", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + fix.conf.MustSet(fix.ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + // We load our identity which we will use to replay the webauth session + fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + browserClient := testhelpers.NewClientWithCookies(t) + body, _, _ := fix.submitWebAuthnLoginWithClient(t, spa, []byte("invalid context"), browserClient, func(values url.Values) { + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel1)) + + if spa { + assert.Equal( + t, + "Expected WebAuthN in internal context to be an object but got: unexpected end of JSON input", + gjson.Get(body, "error.reason").String(), + "%s", body, + ) + } else { + assert.Equal( + t, + "Expected WebAuthN in internal context to be an object but got: unexpected end of JSON input", + gjson.Get(body, "reason").String(), + "%s", body, + ) + } + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=succeeds with passwordless login", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + fix.conf.MustSet(fix.ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + // We load our identity which we will use to replay the webauth session + id := fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + browserClient := testhelpers.NewClientWithCookies(t) + body, res, f := fix.submitWebAuthnLoginWithClient(t, spa, loginPasswordlessContext, browserClient, func(values url.Values) { + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel1)) + + prefix := "" + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), fix.redirTS.URL) + } + + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypePasskey, gjson.Get(body, prefix+"authentication_methods.#(method==passkey).method").String(), "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body) + + actualFlow, err := fix.reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) + } + + // We test here that login works even if the identity schema contains + // { webauthn: { identifier: true } } instead of + // { passkey: { display_name: true } } + t.Run("webauthn_identifier", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/login_webauthn.schema.json") + t.Run("type=browser", func(t *testing.T) { run(t, false) }) + t.Run("type=spa", func(t *testing.T) { run(t, true) }) + }) + t.Run("passkey_display_name", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/login.schema.json") + t.Run("type=browser", func(t *testing.T) { run(t, false) }) + t.Run("type=spa", func(t *testing.T) { run(t, true) }) + }) + }) + }) + + t.Run("flow=refresh", func(t *testing.T) { + fix := newLoginFixture(t) + fix.conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + loginFixtureSuccessEmail := gjson.GetBytes(loginSuccessIdentity, "traits.email").String() + + run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) { + body, res, f := fix.submitWebAuthnLogin(t, isSPA, id, context, func(values url.Values) { + values.Set("identifier", loginFixtureSuccessEmail) + values.Set(node.PasskeyLogin, string(response)) + }, testhelpers.InitFlowWithRefresh()) + snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "2.attributes.nonce", + "2.attributes.src", + "5.attributes.value", + }) + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + assert.Equal(t, loginFixtureSuccessEmail, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").String(), "%s", nodes) + + prefix := "" + if isSPA { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow, "%s", body) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), fix.redirTS.URL, "%s", body) + } + + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + + assert.EqualValues(t, expectedAAL, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypePasskey, gjson.Get(body, prefix+"authentication_methods.#(method==passkey).method").String(), "%s", body) + assert.Len(t, gjson.Get(body, prefix+"authentication_methods").Array(), 2, "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body) + } + + expectedAAL := identity.AuthenticatorAssuranceLevel1 + + for _, tc := range []struct { + creds identity.Credentials + response []byte + context []byte + descript string + }{ + { + creds: identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }, + context: loginPasswordlessContext, + response: loginPasswordlessResponse, + descript: "passwordless credentials", + }, + } { + t.Run("case=refresh "+tc.descript, func(t *testing.T) { + id := fix.createIdentityWithPasskey(t, tc.creds) + + for _, f := range []string{ + "browser", + "spa", + } { + t.Run(f, func(t *testing.T) { + run(t, id, tc.context, tc.response, f == "spa", expectedAAL) + }) + } + }) + } + }) +} diff --git a/selfservice/strategy/passkey/passkey_registration.go b/selfservice/strategy/passkey/passkey_registration.go new file mode 100644 index 000000000000..88efd420d725 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_registration.go @@ -0,0 +1,322 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + _ "embed" + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + jsonschema "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/x/randx" +) + +// Update Registration Flow with Passkey Method +// +// swagger:model updateRegistrationFlowWithPasskeyMethod +type updateRegistrationFlowWithPasskeyMethod struct { + // Register a WebAuthn Security Key + // + // It is expected that the JSON returned by the WebAuthn registration process + // is included here. + Register string `json:"passkey_register"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // The identity's traits + // + // required: true + Traits json.RawMessage `json:"traits"` + + // Method + // + // Should be set to "passkey" when trying to add, update, or remove a Passkey. + // + // required: true + Method string `json:"method"` + + // Flow is flow ID. + // + // swagger:ignore + Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty"` +} + +func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) { + webauthnx.RegisterWebauthnRoute(r) +} + +func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithPasskeyMethod, err error) error { + if f != nil { + if p != nil { + for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } + + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +func (s *Strategy) decode(r *http.Request) (*updateRegistrationFlowWithPasskeyMethod, error) { + var p updateRegistrationFlowWithPasskeyMethod + err := registration.DecodeBody(&p, r, s.hd, s.d.Config(), registrationSchema) + return &p, err +} + +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, ident *identity.Identity) (err error) { + ctx := r.Context() + + if regFlow.Type != flow.TypeBrowser { + return flow.ErrStrategyNotResponsible + } + + params, err := s.decode(r) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + regFlow.TransientPayload = params.TransientPayload + + if params.Register == "" || + params.Register == "true" { // The React SDK sends "true" on empty values, so we ignore these. + return flow.ErrStrategyNotResponsible + } + + if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, params.CSRFToken); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + params.Method = s.ID().String() + if err := flow.MethodEnabledAndAllowed(ctx, regFlow.GetFlowName(), params.Method, params.Method, s.d); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + if len(params.Traits) == 0 { + params.Traits = json.RawMessage("{}") + } + ident.Traits = identity.Traits(params.Traits) + + webAuthnSession := gjson.GetBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if !webAuthnSession.IsObject() { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object."))) + } + var webAuthnSess webauthn.SessionData + if err := json.Unmarshal([]byte(webAuthnSession.Raw), &webAuthnSess); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) + } + + if webAuthnSess.UserID == nil || len(webAuthnSess.UserID) == 0 { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN session data to contain a user ID"))) + } + + webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(params.Register)) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err))) + } + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config").WithDebug(err.Error()))) + } + + credential, err := webAuthn.CreateCredential(&webauthnx.User{ + ID: webAuthnSess.UserID, + Config: webAuthn.Config, + }, webAuthnSess, webAuthnResponse) + if err != nil { + if devErr := new(protocol.Error); errors.As(err, &devErr) { + s.d.Logger().WithError(err).WithField("error_devinfo", devErr.DevInfo).Error("Failed to create WebAuthn credential") + } + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err))) + } + + credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true) + credentialWebAuthnConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{ + Credentials: identity.CredentialsWebAuthn{*credentialWebAuthn}, + UserHandle: webAuthnSess.UserID, + }) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))) + } + + ident.UpsertCredentialsConfig(s.ID(), credentialWebAuthnConfig, 1) + passkeyCred, _ := ident.GetCredentials(s.ID()) + passkeyCred.Identifiers = []string{string(webAuthnSess.UserID)} + ident.SetCredentials(s.ID(), *passkeyCred) + if err := s.validateCredentials(ctx, ident); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + return nil +} + +type passkeyCreateData struct { + CredentialOptions *protocol.CredentialCreation `json:"credentialOptions"` + DisplayNameFieldName string `json:"displayNameFieldName"` +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registration.Flow) error { + ctx := r.Context() + if regFlow.Type != flow.TypeBrowser { + return nil + } + + defaultSchemaURL, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx) + if err != nil { + return err + } + nodes, err := s.populateRegistrationNodes(ctx, defaultSchemaURL) + if err != nil { + return err + } + + for _, n := range nodes { + regFlow.UI.SetNode(n) + } + + // Passkey nodes begin + createData := new(passkeyCreateData) + + fieldName, err := s.PasskeyDisplayNameFromSchema(ctx, defaultSchemaURL.String()) + if err != nil { + return err + } + createData.DisplayNameFieldName = fieldName + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + user := &webauthnx.User{ + Name: "", + ID: []byte(randx.MustString(64, randx.AlphaNum)), + Config: s.d.Config().PasskeyConfig(ctx), + } + option, sessionData, err := webAuthn.BeginRegistration(user) + if err != nil { + return errors.WithStack(err) + } + createData.CredentialOptions = option + + injectWebAuthnOptions, err := json.Marshal(createData) + if err != nil { + return errors.WithStack(err) + } + + regFlow.InternalContext, err = sjson.SetBytes( + regFlow.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + regFlow.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + regFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyCreateData, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + regFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyRegister, + Type: node.InputAttributeTypeHidden, + }}) + + regFlow.UI.Nodes.Append(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{Label: text.NewInfoSelfServiceRegistrationRegisterPasskey()}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyRegisterTrigger, + Type: node.InputAttributeTypeButton, + OnClick: "window.__oryPasskeyRegistration()", // defined in webauthn.js + }}) + + // Passkey nodes end + + regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + return nil +} + +func (s *Strategy) populateRegistrationNodes(ctx context.Context, schemaURL *url.URL) (node.Nodes, error) { + runner, err := schema.NewExtensionRunner(ctx) + if err != nil { + return nil, err + } + c := jsonschema.NewCompiler() + runner.Register(c) + + nodes, err := container.NodesFromJSONSchema(ctx, node.DefaultGroup, schemaURL.String(), "", c) + if err != nil { + return nil, err + } + + return nodes, nil +} + +func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity) error { + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { + return err + } + + c := i.GetCredentialsOr(identity.CredentialsTypePasskey, &identity.Credentials{}) + if len(c.Identifiers) == 0 { + return schema.NewMissingIdentifierError() + } + + return nil +} diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go new file mode 100644 index 000000000000..a6ab50b29b61 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -0,0 +1,409 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + _ "embed" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal/registrationhelpers" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/randx" + "github.com/ory/x/sqlxx" +) + +var ( + flows = []string{"spa", "browser"} + + //go:embed fixtures/registration/success/response.json + registrationFixtureSuccessResponse []byte + //go:embed fixtures/registration/success/internal_context.json + registrationFixtureSuccessInternalContext []byte + //go:embed fixtures/registration/failure/internal_context_missing_user_id.json + registrationFixtureFailureInternalContextMissingUserID []byte + //go:embed fixtures/registration/failure/internal_context_wrong_user_id.json + registrationFixtureFailureInternalContextWrongUserID []byte +) + +func flowIsSPA(flow string) bool { + return flow == "spa" +} + +func TestRegistration(t *testing.T) { + t.Parallel() + + t.Run("AssertCommonErrorCases", func(t *testing.T) { + registrationhelpers.AssertCommonErrorCases(t, flows) + }) + + t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) { + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertCSRFFailures", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) { + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertSchemaDoesNotExist", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertSchemDoesNotExist(t, reg, flows, func(v url.Values) { + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("case=passkey button does not exist when passwordless is disabled", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyEnabled, false) + t.Cleanup(func() { fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyEnabled, true) }) + for _, flowType := range flows { + flowType := flowType + t.Run(flowType, func(t *testing.T) { + t.Parallel() + client := testhelpers.NewClientWithCookies(t) + flo := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, flowIsSPA(flowType), false, false) + testhelpers.SnapshotTExcept(t, flo.Ui.Nodes, []string{ + "0.attributes.value", + }) + }) + } + }) + + t.Run("case=passkey button exists", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + for _, flowType := range flows { + flowType := flowType + t.Run(flowType, func(t *testing.T) { + t.Parallel() + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, flowIsSPA(flowType), false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "2.attributes.value", + "3.attributes.src", + "3.attributes.nonce", + "6.attributes.value", + }) + }) + } + }) + + t.Run("case=should return an error because not passing validation", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t, fix.publicTS, fix.conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual) + assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + }) + } + }) + + t.Run("case=should reject invalid transient payload", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bar") + v.Set("transient_payload", "42") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t, fix.publicTS, fix.conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Equal(t, "bar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual) + assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + assert.Equal(t, int64(4000026), gjson.Get(actual, "ui.nodes.#(attributes.name==transient_payload).messages.0.id").Int(), "%s", actual) + }) + } + }) + + t.Run("case=should return an error because passkey response is invalid", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, "invalid") + v.Set("method", "passkey") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := fix.submitPasskeyRegistration(t, f, testhelpers.NewClientWithCookies(t), values) + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), node.PasskeyRegister, "csrf_token", "traits.username", "traits.foobar") + assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual) + assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0").String(), `Unable to parse WebAuthn response: Parse error for Registration`, "%s", actual) + }) + } + }) + + t.Run("case=should return an error because internal context is invalid", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + for _, tc := range []struct { + name string + internalContext string + }{{ + name: "invalid json", + internalContext: "invalid", + }, { + name: "missing user ID", + internalContext: string(registrationFixtureFailureInternalContextMissingUserID), + }, { + name: "wrong user ID", + internalContext: string(registrationFixtureFailureInternalContextWrongUserID), + }} { + tc := tc + t.Run("context="+tc.name, func(t *testing.T) { + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := fix.submitPasskeyRegistration(t, f, testhelpers.NewClientWithCookies(t), values, + withInternalContext(sqlxx.JSONRawMessage(tc.internalContext))) + if flowIsSPA(f) { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "error.status").String(), "%s", actual) + } else { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual) + } + }) + } + }) + } + }) + + t.Run("case=should fail to create identity if schema is missing the identifier", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/noid.schema.json") + email := testhelpers.RandomEmail() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + isSPA := f == "spa" + regFlow := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, isSPA, false, false) + + // fill out traits and click on "sign up with passkey" + urlValues := testhelpers.SDKFormFieldsToURLValues(regFlow.Ui.Nodes) + urlValues.Set("traits.email", email) + urlValues.Set("method", "passkey") + actual, _ := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, urlValues.Encode()) + + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.email") + assert.Equal(t, text.NewErrorValidationRegistrationNoStrategyFound().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + getPrefix := func(f string) (prefix string) { + if f == "spa" { + prefix = "session." + } + return + } + + t.Run("successful registration", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + t.Cleanup(fix.disableSessionAfterRegistration) + + var values = func(email string) func(v url.Values) { + return func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + } + + t.Run("case=should create the identity but not a session", func(t *testing.T) { + fix.useRedirNoSessionTS() + t.Cleanup(fix.useRedirTS) + fix.disableSessionAfterRegistration() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := f + "-" + testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirNoSessionTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + + if f == "spa" { + assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } else { + assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + + t.Run("case=should accept valid transient payload", func(t *testing.T) { + fix.useRedirNoSessionTS() + t.Cleanup(fix.useRedirTS) + fix.disableSessionAfterRegistration() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirNoSessionTS.URL+"/registration-return-ts", func(v url.Values) { + values(email)(v) + v.Set("transient_payload.stuff", "42") + }, withUserID(userID)) + + if f == "spa" { + assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } else { + assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + + t.Run("case=should create the identity and a session and use the correct schema", func(t *testing.T) { + fix.enableSessionAfterRegistration() + fix.conf.MustSet(fix.ctx, config.ViperKeyDefaultIdentitySchemaID, "advanced-user") + fix.conf.MustSet(fix.ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "does-not-exist", URL: "file://./stub/profile.schema.json"}, + {ID: "advanced-user", URL: "file://./stub/registration.schema.json"}, + }) + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + + prefix := getPrefix(f) + + assert.Equal(t, email, gjson.Get(actual, prefix+"identity.traits.username").String(), "%s", actual) + assert.True(t, gjson.Get(actual, prefix+"active").Bool(), "%s", actual) + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + + t.Run("case=not able to create the same account twice", func(t *testing.T) { + fix.enableSessionAfterRegistration() + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + assert.True(t, gjson.Get(actual, getPrefix(f)+"active").Bool(), "%s", actual) + + actual, _, _ = fix.makeRegistration(t, f, values(email)) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username") + assert.Equal(t, + "You tried signing in with "+email+" which is already in use by another account. You can sign in using your password.", + gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + t.Run("case=reset previous form errors", func(t *testing.T) { + fix.enableSessionAfterRegistration() + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + actual, _, _ := fix.makeRegistration(t, f, func(v url.Values) { + v.Del("traits.username") + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + }) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages.0").String(), `Property username is missing`, "%s", actual) + + actual, _, _ = fix.makeRegistration(t, f, func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + }) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages").Array()) + assert.Empty(t, gjson.Get(actual, "ui.nodes.messages").Array()) + }) + } + }) + }) +} diff --git a/selfservice/strategy/passkey/passkey_schema_extension.go b/selfservice/strategy/passkey/passkey_schema_extension.go new file mode 100644 index 000000000000..a3d7b32c6e6d --- /dev/null +++ b/selfservice/strategy/passkey/passkey_schema_extension.go @@ -0,0 +1,111 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/x/stringsx" +) + +type SchemaExtension struct { + WebauthnIdentifier string + PasskeyDisplayName string + sync.Mutex +} + +func (e *SchemaExtension) Run(_ jsonschema.ValidationContext, s schema.ExtensionConfig, value any) error { + e.Lock() + defer e.Unlock() + + if s.Credentials.WebAuthn.Identifier { + e.WebauthnIdentifier = strings.ToLower(fmt.Sprintf("%s", value)) + } + + if s.Credentials.Passkey.DisplayName { + e.PasskeyDisplayName = fmt.Sprintf("%s", value) + } + + return nil +} + +func (e *SchemaExtension) Finish() error { return nil } + +// PasskeyDisplayNameFromIdentity returns the passkey display name from the +// identity. It is usually the email address and used to name the passkey in the +// browser. +func (s *Strategy) PasskeyDisplayNameFromIdentity(ctx context.Context, id *identity.Identity) string { + e := new(SchemaExtension) + // We can ignore teh error here because proper validation happens once the identity is persisted. + _ = s.d.IdentityValidator().ValidateWithRunner(ctx, id, e) + + return stringsx.Coalesce(e.PasskeyDisplayName, e.WebauthnIdentifier) +} + +func (s *Strategy) PasskeyDisplayNameFromTraits(ctx context.Context, traits identity.Traits) string { + id := identity.NewIdentity("") + id.Traits = traits + + return s.PasskeyDisplayNameFromIdentity(ctx, id) +} + +func (s *Strategy) PasskeyDisplayNameFromSchema(ctx context.Context, schemaURL string) (string, error) { + ext := &passkeyDisplayNameExtension{} + + runner, err := schema.NewExtensionRunner(ctx, schema.WithCompileRunners(ext)) + if err != nil { + return "", err + } + c := jsonschema.NewCompiler() + c.ExtractAnnotations = true + runner.Register(c) + + schem, err := c.Compile(ctx, schemaURL) + if err != nil { + return "", err + } + + for key, value := range schem.Properties["traits"].Properties { + if value.Title == ext.getLabel() { + return "traits." + key, nil + } + } + + return "", errors.New("no identifier found") +} + +type passkeyDisplayNameExtension struct { + identifierLabelCandidates []string +} + +func (i *passkeyDisplayNameExtension) Run(_ jsonschema.CompilerContext, config schema.ExtensionConfig, rawSchema map[string]interface{}) error { + if config.Credentials.WebAuthn.Identifier || + config.Credentials.Passkey.DisplayName { + if title, ok := rawSchema["title"]; ok { + // The jsonschema compiler validates the title to be a string, so this should always work. + switch t := title.(type) { + case string: + if t != "" { + i.identifierLabelCandidates = append(i.identifierLabelCandidates, t) + } + } + } + } + return nil +} + +func (i *passkeyDisplayNameExtension) getLabel() string { + if len(i.identifierLabelCandidates) != 1 { + // sane default is set elsewhere + return "" + } + return i.identifierLabelCandidates[0] +} diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go new file mode 100644 index 000000000000..05d49ab56f63 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_settings.go @@ -0,0 +1,419 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + _ "embed" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/x/decoderx" + "github.com/ory/x/randx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +func (s *Strategy) RegisterSettingsRoutes(_ *x.RouterPublic) {} + +func (s *Strategy) SettingsStrategyID() string { return s.ID().String() } + +const ( + InternalContextKeySessionData = "session_data" +) + +func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity, f *settings.Flow) error { + if f.Type != flow.TypeBrowser { + return nil + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + confidentialIdentity, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + if err != nil { + return err + } + + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), confidentialIdentity) + if err != nil { + return err + } + + if webAuthns, err := s.identityListWebAuthn(confidentialIdentity); errors.Is(err, sqlcon.ErrNoRows) { + // Do nothing + } else if err != nil { + return err + } else { + for k := range webAuthns.Credentials { + // We only show the option to remove a credential, if it is not the last one when passwordless, + // or, if it is for MFA we show it always. + cred := &webAuthns.Credentials[k] + + f.UI.Nodes.Append(webauthnx.NewPasskeyUnlink(cred, func(a *node.InputAttributes) { + // Do not remove this node if it is the last credential the identity can sign in with. + a.Disabled = count < 2 + })) + } + } + + web, err := webauthn.New(s.d.Config().PasskeyConfig(r.Context())) + if err != nil { + return errors.WithStack(err) + } + + identifier := s.PasskeyDisplayNameFromIdentity(r.Context(), id) + if identifier == "" { + f.UI.Messages.Add(text.NewErrorValidationIdentifierMissing()) + return nil + } + + user := &webauthnx.User{ + Name: identifier, + ID: []byte(randx.MustString(64, randx.AlphaNum)), + Config: s.d.Config().PasskeyConfig(r.Context()), + } + option, sessionData, err := web.BeginRegistration(user) + if err != nil { + return errors.WithStack(err) + } + + f.InternalContext, err = sjson.SetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), sessionData) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(r.Context()))) + + f.UI.Nodes.Upsert(node.NewInputField( + node.PasskeyRegisterTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(a *node.InputAttributes) { + a.OnClick = "window.__oryPasskeySettingsRegistration()" + }), + ).WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterPasskey())) + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeySettingsRegister, + Type: node.InputAttributeTypeHidden, + }}) + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyCreateData, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + return nil +} + +func (s *Strategy) identityListWebAuthn(id *identity.Identity) (*identity.CredentialsWebAuthnConfig, error) { + cred, ok := id.GetCredentials(s.ID()) + if !ok { + return nil, errors.WithStack(sqlcon.ErrNoRows) + } + + var cc identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + return nil, errors.WithStack(err) + } + + return &cc, nil +} + +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { + if f.Type != flow.TypeBrowser { + return nil, flow.ErrStrategyNotResponsible + } + var p updateSettingsFlowWithPasskeyMethod + ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) + if errors.Is(err, settings.ErrContinuePreviousAction) { + return ctxUpdate, s.continueSettingsFlow(w, r, ctxUpdate, &p) + } else if err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + } + + if err := s.decodeSettingsFlow(r, &p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + } + + if len(p.Register+p.Remove) > 0 { + // This method has only two submit buttons + p.Method = s.SettingsStrategyID() + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) + } + } else { + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + + // This does not come from the payload! + p.Flow = ctxUpdate.Flow.ID.String() + if err := s.continueSettingsFlow(w, r, ctxUpdate, &p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + } + + return ctxUpdate, nil +} + +// Update Settings Flow with Passkey Method +// +// swagger:model updateSettingsFlowWithPasskeyMethod +type updateSettingsFlowWithPasskeyMethod struct { + // Register a WebAuthn Security Key + // + // It is expected that the JSON returned by the WebAuthn registration process + // is included here. + Register string `json:"passkey_settings_register"` + + // Remove a WebAuthn Security Key + // + // This must contain the ID of the WebAuthN connection. + Remove string `json:"passkey_remove"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // Method + // + // Should be set to "passkey" when trying to add, update, or remove a webAuthn pairing. + // + // required: true + Method string `json:"method"` + + // Flow is flow ID. + // + // swagger:ignore + Flow string `json:"flow"` +} + +func (p *updateSettingsFlowWithPasskeyMethod) GetFlowID() uuid.UUID { + return x.ParseUUID(p.Flow) +} + +func (p *updateSettingsFlowWithPasskeyMethod) SetFlowID(rid uuid.UUID) { + p.Flow = rid.String() +} + +func (s *Strategy) continueSettingsFlow( + w http.ResponseWriter, r *http.Request, + ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasskeyMethod, +) error { + if len(p.Register+p.Remove) > 0 { + if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + return err + } + + if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return err + } + + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { + return errors.WithStack(settings.NewFlowNeedsReAuth()) + } + } else { + return errors.New("ended up in unexpected state") + } + + switch { + case len(p.Remove) > 0: + return s.continueSettingsFlowRemove(w, r, ctxUpdate, p) + case len(p.Register) > 0: + return s.continueSettingsFlowAdd(r, ctxUpdate, p) + default: + return errors.New("ended up in unexpected state") + } +} + +func (s *Strategy) continueSettingsFlowRemove(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasskeyMethod) error { + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.IdentityID) + if err != nil { + return err + } + + cred, ok := i.GetCredentials(s.ID()) + if !ok { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You tried to remove a WebAuthn but you have no WebAuthn set up.")) + } + + var cc identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + updated := make([]identity.CredentialWebAuthn, 0) + for k, cred := range cc.Credentials { + if fmt.Sprintf("%x", cred.ID) != p.Remove { + updated = append(updated, cc.Credentials[k]) + } + } + + if len(updated) == len(cc.Credentials) { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You tried to remove a passkey which does not exist.")) + } + + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), i) + if err != nil { + return err + } + + if count < 2 { + return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(webauthnx.ErrNotEnoughCredentials)) + } + + if len(updated) == 0 { + i.DeleteCredentialsType(identity.CredentialsTypePasskey) + ctxUpdate.UpdateIdentity(i) + return nil + } + + cc.Credentials = updated + cred.Config, err = json.Marshal(cc) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + + i.SetCredentials(s.ID(), *cred) + ctxUpdate.UpdateIdentity(i) + return nil +} + +func (s *Strategy) continueSettingsFlowAdd(r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasskeyMethod) error { + webAuthnSession := gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if !webAuthnSession.IsObject() { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object.")) + } + + var webAuthnSess webauthn.SessionData + if err := json.Unmarshal([]byte(gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)).Raw), &webAuthnSess); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err)) + } + + webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(p.Register)) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err)) + } + + web, err := webauthn.New(s.d.Config().PasskeyConfig(r.Context())) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())) + } + + credential, err := web.CreateCredential(&webauthnx.User{ + ID: webAuthnSess.UserID, + Config: web.Config, + }, webAuthnSess, webAuthnResponse) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err)) + } + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.IdentityID) + if err != nil { + return err + } + + cred := i.GetCredentialsOr(s.ID(), &identity.Credentials{Config: sqlxx.JSONRawMessage("{}")}) + + var cc identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true) + cc.UserHandle = webAuthnSess.UserID + cc.Credentials = append(cc.Credentials, *credentialWebAuthn) + credentialsConfig, err := json.Marshal(cc) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + + i.UpsertCredentialsConfig(s.ID(), credentialsConfig, 1, identity.WithAdditionalIdentifier(string(webAuthnSess.UserID))) + if err := s.validateCredentials(r.Context(), i); err != nil { + return err + } + + // Remove the WebAuthn URL from the internal context now that it is set! + ctxUpdate.Flow.InternalContext, err = sjson.DeleteBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if err != nil { + return err + } + + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), ctxUpdate.Flow); err != nil { + return err + } + + aal := identity.AuthenticatorAssuranceLevel1 + + // Since we added the method, it also means that we have authenticated it + if err := s.d.SessionManager().SessionAddAuthenticationMethods(r.Context(), ctxUpdate.Session.ID, session.AuthenticationMethod{ + Method: s.ID(), + AAL: aal, + }); err != nil { + return err + } + + ctxUpdate.UpdateIdentity(i) + return nil +} + +func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error { + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(settingsSchema) + if err != nil { + return errors.WithStack(err) + } + + return decoderx.NewHTTP().Decode(r, dest, compiler, + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasskeyMethod, err error) error { + // Do not pause flow if the flow type is an API flow as we can't save cookies in those flows. + if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser { + if err := s.d.ContinuityManager().Pause(r.Context(), w, r, settings.ContinuityKey(s.SettingsStrategyID()), settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity())...); err != nil { + return err + } + } + + if ctxUpdate.Flow != nil { + ctxUpdate.Flow.UI.ResetMessages() + ctxUpdate.Flow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + + return err +} diff --git a/selfservice/strategy/passkey/passkey_settings_test.go b/selfservice/strategy/passkey/passkey_settings_test.go new file mode 100644 index 000000000000..ced111071711 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_settings_test.go @@ -0,0 +1,451 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/strategy/passkey" + + "github.com/ory/x/snapshotx" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + + "github.com/stretchr/testify/require" + + "github.com/ory/x/assertx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/x" +) + +//go:embed fixtures/settings/success/identity.json +var settingsFixtureSuccessIdentity []byte + +//go:embed fixtures/settings/success/response.json +var settingsFixtureSuccessResponse []byte + +//go:embed fixtures/settings/success/internal_context.json +var settingsFixtureSuccessInternalContext []byte + +var ctx = context.Background() + +func TestCompleteSettings(t *testing.T) { + fix := newSettingsFixture(t) + + t.Run("case=invalid passkey config", func(t *testing.T) { + fix := newSettingsFixture(t) + fix.conf.MustSet(ctx, config.ViperKeyPasskeyRPID, "") + id := fix.createIdentity(t) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id) + + req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + res, err := apiClient.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + }) + + t.Run("case=a device is shown which can be unlinked", func(t *testing.T) { + id := fix.createIdentity(t) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "4.attributes.value", // passkey_settings_register + "5.attributes.value", // CSRF + "6.attributes.nonce", // script + "6.attributes.src", // script + }) + }) + + t.Run("case=invalid credentials", func(t *testing.T) { + id, _ := fix.createIdentityAndReturnIdentifier(t, []byte(`{invalid}`)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id) + + req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + res, err := apiClient.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + }) + + t.Run("case=one activation element is shown", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "2.attributes.value", // passkey_create_data + "3.attributes.value", // CSRF + "4.attributes.nonce", // script + "4.attributes.src", // script + }) + }) + + t.Run("case=passkeys only work for browsers", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS) + for _, n := range f.Ui.Nodes { + assert.NotEqual(t, n.Group, "passkey", "unexpected group: %s", n.Group) + } + }) + + doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v(values) + payload := testhelpers.EncodeFormAsJSON(t, true, values) + return testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, payload) + } + + doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v(values) + return testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + } + + t.Run("case=fails with api submit because only browsers are supported", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + body, res := doAPIFlow(t, func(v url.Values) { + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationSettingsNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("case=fails with browser submit because csrf token is missing", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentityWithoutPasskey(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, x.ErrInvalidCSRFToken.Reason(), gjson.Get(body, "error.reason").String(), body) + } else { + assert.Contains(t, res.Request.URL.String(), fix.errTS.URL) + assert.Equal(t, x.ErrInvalidCSRFToken.Reason(), gjson.Get(body, "reason").String(), body) + } + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=fails with browser submit register payload is invalid", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentityWithoutPasskey(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Parse error for Registration", "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=requires privileged session for register", func(t *testing.T) { + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + t.Cleanup(func() { + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + }) + + run := func(t *testing.T, spa bool) { + id := fix.createIdentityWithoutPasskey(t) + + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + + if spa { + assert.NotEmpty(t, gjson.Get(body, "redirect_browser_to").String()) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(body), []string{"redirect_browser_to"}) + } else { + assert.Contains(t, res.Request.URL.String(), fix.loginTS.URL+"/login-ts") + assertx.EqualAsJSON(t, text.NewInfoLoginReAuth(), json.RawMessage(gjson.Get(body, "ui.messages.0").Raw)) + } + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=add a passkey", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + // We load our identity which we will use to replay the webauth session + var id identity.Identity + require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id)) + _ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, &id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS) + + // We inject the session to replay + interim, err := fix.reg.SettingsFlowPersister().GetSettingsFlow(fix.ctx, uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = settingsFixtureSuccessInternalContext + require.NoError(t, fix.reg.SettingsFlowPersister().UpdateSettingsFlow(fix.ctx, interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + // We use the response replay + values.Set("method", "passkey") + values.Set(node.PasskeySettingsRegister, string(settingsFixtureSuccessResponse)) + body, res := testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypePasskey) + assert.True(t, ok) + assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1) + + actualFlow, err := fix.reg.SettingsFlowPersister().GetSettingsFlow(fix.ctx, uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + // new session data has been generated + assert.NotEqual(t, settingsFixtureSuccessInternalContext, + gjson.GetBytes(actualFlow.InternalContext, + flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) + + testhelpers.EnsureAAL(t, browserClient, fix.publicTS, "aal1", string(identity.CredentialsTypePasskey)) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=fails to remove passkey if it is the last credential available", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + id.DeleteCredentialsType(identity.CredentialsTypePassword) + conf := sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`) + id.UpsertCredentialsConfig(identity.CredentialsTypePasskey, conf, 0) + require.NoError(t, fix.reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits)) + + body, res := doBrowserFlow(t, spa, func(v url.Values) { + // The remove key should be empty + snapshotx.SnapshotT(t, v, snapshotx.ExceptPaths("csrf_token", "passkey_create_data")) + v.Set(node.PasskeyRemove, "666f6f666f6f") + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + + t.Run("response", func(t *testing.T) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(body, "state").String(), body) + snapshotx.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes.#(attributes.name==passkey_remove)").String()), nil) + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypePasskey) + assert.True(t, ok) + assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1) + }) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=remove all passkeys", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + allCred, ok := id.GetCredentials(identity.CredentialsTypePasskey) + assert.True(t, ok) + + var cc identity.CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(allCred.Config, &cc)) + require.Len(t, cc.Credentials, 2) + + for _, cred := range cc.Credentials { + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeyRemove, fmt.Sprintf("%x", cred.ID)) + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + } + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + _, ok = actual.GetCredentials(identity.CredentialsTypePasskey) + assert.False(t, ok) + // Check not to remove other credentials with webauthn + _, ok = actual.GetCredentials(identity.CredentialsTypePassword) + assert.True(t, ok) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=fails with browser submit register payload is invalid", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeyRemove, fmt.Sprintf("%x", []byte("foofoo"))) + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(body, "state").String())) + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypePasskey) + assert.True(t, ok) + assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1) + assert.Equal(t, "bar", gjson.GetBytes(cred.Config, "credentials.0.display_name").String()) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=is not responsible if neither remove or register is set", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeyRemove, "") + v.Set(node.PasskeyRegister, "") + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + + assert.Equal(t, text.NewErrorValidationSettingsNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://stub/missing-identifier.schema.json") + + for _, f := range []string{"spa", "browser"} { + t.Run("type="+f, func(t *testing.T) { + isSPA := f == "spa" + + var id identity.Identity + require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id)) + _ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, &id) + + req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + if isSPA { + req.Header.Set("Accept", "application/json") + } + res, err := browserClient.Do(req) + require.NoError(t, err) + + actual := x.MustReadAll(res.Body) + defer res.Body.Close() + + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.GetBytes(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) +} diff --git a/selfservice/strategy/passkey/passkey_strategy.go b/selfservice/strategy/passkey/passkey_strategy.go new file mode 100644 index 000000000000..dbf550d352ff --- /dev/null +++ b/selfservice/strategy/passkey/passkey_strategy.go @@ -0,0 +1,117 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + "encoding/json" + + "github.com/pkg/errors" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +type strategyDependencies interface { + x.LoggingProvider + x.WriterProvider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + + config.Provider + + continuity.ManagementProvider + + errorx.ManagementProvider + hash.HashProvider + + registration.HandlerProvider + registration.HooksProvider + registration.ErrorHandlerProvider + registration.HookExecutorProvider + registration.FlowPersistenceProvider + + login.HooksProvider + login.ErrorHandlerProvider + login.HookExecutorProvider + login.FlowPersistenceProvider + login.HandlerProvider + + settings.FlowPersistenceProvider + settings.HookExecutorProvider + settings.HooksProvider + settings.ErrorHandlerProvider + + identity.PrivilegedPoolProvider + identity.ValidationProvider + identity.ActiveCredentialsCounterStrategyProvider + identity.ManagementProvider + + session.HandlerProvider + session.ManagementProvider +} + +var ( + _ login.Strategy = new(Strategy) + _ registration.Strategy = new(Strategy) + _ identity.ActiveCredentialsCounter = new(Strategy) +) + +type Strategy struct { + d strategyDependencies + hd *decoderx.HTTP +} + +func NewStrategy(d any) *Strategy { + return &Strategy{ + d: d.(strategyDependencies), + hd: decoderx.NewHTTP(), + } +} + +func (*Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypePasskey +} + +func (*Strategy) NodeGroup() node.UiNodeGroup { + return node.PasskeyGroup +} + +func (s *Strategy) CompletedAuthenticationMethod(context.Context, session.AuthenticationMethods) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: identity.CredentialsTypePasskey, + AAL: identity.AuthenticatorAssuranceLevel1, + } +} + +func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return s.countCredentials(cc) +} + +func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return s.countCredentials(cc) +} + +func (s *Strategy) countCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + for _, c := range cc { + if c.Type == s.ID() && len(c.Config) > 0 && len(c.Identifiers) > 0 { + var conf identity.CredentialsWebAuthnConfig + if err = json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + count += len(conf.Credentials) + } + } + return +} diff --git a/selfservice/strategy/passkey/schema.go b/selfservice/strategy/passkey/schema.go new file mode 100644 index 000000000000..8be1150729bc --- /dev/null +++ b/selfservice/strategy/passkey/schema.go @@ -0,0 +1,15 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import _ "embed" + +//go:embed .schema/registration.schema.json +var registrationSchema []byte + +//go:embed .schema/login.schema.json +var loginSchema []byte + +//go:embed .schema/settings.schema.json +var settingsSchema []byte diff --git a/selfservice/strategy/passkey/stub/login.schema.json b/selfservice/strategy/passkey/stub/login.schema.json new file mode 100644 index 000000000000..d6e72ec5fb0f --- /dev/null +++ b/selfservice/strategy/passkey/stub/login.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "website": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + } + }, + "required": [] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/login_webauthn.schema.json b/selfservice/strategy/passkey/stub/login_webauthn.schema.json new file mode 100644 index 000000000000..a94dfcc80d6d --- /dev/null +++ b/selfservice/strategy/passkey/stub/login_webauthn.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "website": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "required": [] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/missing-identifier.schema.json b/selfservice/strategy/passkey/stub/missing-identifier.schema.json new file mode 100644 index 000000000000..43565799bba7 --- /dev/null +++ b/selfservice/strategy/passkey/stub/missing-identifier.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/noid.schema.json b/selfservice/strategy/passkey/stub/noid.schema.json new file mode 100644 index 000000000000..d1dcaa77d138 --- /dev/null +++ b/selfservice/strategy/passkey/stub/noid.schema.json @@ -0,0 +1,18 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/profile.schema.json b/selfservice/strategy/passkey/stub/profile.schema.json new file mode 100644 index 000000000000..2503b33597d7 --- /dev/null +++ b/selfservice/strategy/passkey/stub/profile.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "passkey": { + "identifier": true + } + } + } + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/registration.schema.json b/selfservice/strategy/passkey/stub/registration.schema.json new file mode 100644 index 000000000000..441a72e44fcc --- /dev/null +++ b/selfservice/strategy/passkey/stub/registration.schema.json @@ -0,0 +1,35 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "foobar": { + "type": "string", + "minLength": 2 + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "display_name": true + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "required": ["foobar", "username"] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/settings.schema.json b/selfservice/strategy/passkey/stub/settings.schema.json new file mode 100644 index 000000000000..d6e72ec5fb0f --- /dev/null +++ b/selfservice/strategy/passkey/stub/settings.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "website": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + } + }, + "required": [] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/testfixture_test.go b/selfservice/strategy/passkey/testfixture_test.go new file mode 100644 index 000000000000..d7abf8459fb6 --- /dev/null +++ b/selfservice/strategy/passkey/testfixture_test.go @@ -0,0 +1,366 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlxx" +) + +func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) + enablePasskeyStrategy(conf) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + return reg +} + +func newLoginRegistry(t *testing.T) *driver.RegistryDefault { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) + enablePasskeyStrategy(conf) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + return reg +} + +func enablePasskeyStrategy(conf *config.Config) { + ctx := context.Background() + key := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePasskey) + conf.MustSet(ctx, key+".enabled", true) + conf.MustSet(ctx, key+".config.rp.display_name", "Ory Corp") + conf.MustSet(ctx, key+".config.rp.id", "localhost") + conf.MustSet(ctx, key+".config.rp.origins", []string{"http://localhost:4455"}) +} + +type fixture struct { + ctx context.Context + conf *config.Config + reg *driver.RegistryDefault + + publicTS *httptest.Server + redirTS *httptest.Server + redirNoSessionTS *httptest.Server + uiTS *httptest.Server + errTS *httptest.Server + loginTS *httptest.Server +} + +func newRegistrationFixture(t *testing.T) *fixture { + fix := new(fixture) + fix.ctx = context.Background() + fix.reg = newRegistrationRegistry(t) + fix.conf = fix.reg.Config() + ctx := fix.ctx + + router := x.NewRouterPublic() + fix.publicTS, _ = testhelpers.NewKratosServerWithRouters(t, fix.reg, router, x.NewRouterAdmin()) + + _ = testhelpers.NewErrorTestServer(t, fix.reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, fix.reg) + _ = testhelpers.NewRedirSessionEchoTS(t, fix.reg) + + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + fix.conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + + fix.redirTS = testhelpers.NewRedirSessionEchoTS(t, fix.reg) + fix.redirNoSessionTS = testhelpers.NewRedirNoSessionTS(t, fix.reg) + + fix.useReturnToFromTS(fix.redirTS) + + return fix +} + +func newLoginFixture(t *testing.T) *fixture { + fix := new(fixture) + fix.ctx = context.Background() + fix.reg = newLoginRegistry(t) + fix.conf = fix.reg.Config() + ctx := fix.ctx + + fix.conf.MustSet(ctx, + config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", + false) + + router := x.NewRouterPublic() + fix.publicTS, _ = testhelpers.NewKratosServerWithRouters(t, fix.reg, router, x.NewRouterAdmin()) + + fix.errTS = testhelpers.NewErrorTestServer(t, fix.reg) + fix.uiTS = testhelpers.NewLoginUIFlowEchoServer(t, fix.reg) + fix.loginTS = fix.uiTS + + // Overwrite these two to make it more explicit when tests fail + fix.conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, fix.errTS.URL+"/error-ts") + fix.conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, fix.uiTS.URL+"/login-ts") + + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/login.schema.json") + fix.conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + + fix.redirTS = testhelpers.NewRedirSessionEchoTS(t, fix.reg) + fix.redirNoSessionTS = testhelpers.NewRedirNoSessionTS(t, fix.reg) + + fix.useReturnToFromTS(fix.redirTS) + + return fix +} + +func newSettingsFixture(t *testing.T) *fixture { + fix := newLoginFixture(t) + fix.uiTS = testhelpers.NewSettingsUIFlowEchoServer(t, fix.reg) + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m") + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/settings.schema.json") + fix.conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, "aal1") + fix.conf.MustSet(fix.ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + fix.conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".profile.enabled", false) + + return fix +} + +func (fix *fixture) checkURL(t *testing.T, shouldRedirect bool, res *http.Response) { + if shouldRedirect { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL+"/login-ts") + } else { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow) + } +} + +func (fix *fixture) loginViaAPI(t *testing.T, v func(url.Values), apiClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false, opts...) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v(values) + payload := testhelpers.EncodeFormAsJSON(t, true, values) + return testhelpers.LoginMakeRequest(t, true, false, f, apiClient, payload) +} + +func (fix *fixture) loginViaBrowser(t *testing.T, spa bool, cb func(url.Values), browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, fix.publicTS, false, spa, false, false, opts...) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + cb(values) + return testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode()) +} + +func (fix *fixture) createIdentityWithPasskey(t *testing.T, c identity.Credentials) *identity.Identity { + var id identity.Identity + require.NoError(t, json.Unmarshal(loginSuccessIdentity, &id)) + + id.SetCredentials(identity.CredentialsTypePasskey, identity.Credentials{ + Identifiers: []string{"some-random-user-handle"}, + Config: c.Config, + Type: identity.CredentialsTypePasskey, + Version: c.Version, + }) + + // We clean up the identity in case it has been created before + _ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID) + + require.NoError(t, fix.reg.PrivilegedIdentityPool().CreateIdentity(fix.ctx, &id)) + + return &id +} + +func (fix *fixture) submitWebAuthnLoginFlowWithClient(t *testing.T, isSPA bool, f *kratos.LoginFlow, contextFixture []byte, client *http.Client, cb func(values url.Values)) (string, *http.Response, *kratos.LoginFlow) { + // We inject the session to replay + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(fix.ctx, uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = contextFixture + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(fix.ctx, interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + cb(values) + + // We use the response replay + body, res := testhelpers.LoginMakeRequest(t, false, isSPA, f, client, values.Encode()) + return body, res, f +} + +func (fix *fixture) submitWebAuthnLoginWithClient(t *testing.T, isSPA bool, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, fix.publicTS, false, isSPA, false, false, opts...) + return fix.submitWebAuthnLoginFlowWithClient(t, isSPA, f, contextFixture, client, cb) +} + +func (fix *fixture) submitWebAuthnLogin(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) { + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id) + return fix.submitWebAuthnLoginWithClient(t, isSPA, contextFixture, browserClient, cb, opts...) +} + +// useReturnToFromTS sets the "return to" server, which will assert the session +// state (redirTS: enforce that a session exists, redirNoSessionTS: enforce that +// no session exists) +func (fix *fixture) useReturnToFromTS(ts *httptest.Server) { + fix.conf.MustSet(fix.ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to") + fix.conf.MustSet(fix.ctx, config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts") +} +func (fix *fixture) useRedirTS() { fix.useReturnToFromTS(fix.redirTS) } +func (fix *fixture) useRedirNoSessionTS() { fix.useReturnToFromTS(fix.redirNoSessionTS) } + +func (fix *fixture) disableSessionAfterRegistration() { + fix.conf.MustSet(fix.ctx, config.HookStrategyKey( + config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypePasskey.String(), + ), nil) +} +func (fix *fixture) enableSessionAfterRegistration() { + fix.conf.MustSet(fix.ctx, config.HookStrategyKey( + config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypePasskey.String(), + ), []config.SelfServiceHook{{Name: "session"}}) +} + +type submitPasskeyOpt struct { + initFlowOpts []testhelpers.InitFlowWithOption + userID string + internalContext sqlxx.JSONRawMessage +} + +func newSubmitPasskeyOpt() *submitPasskeyOpt { + return &submitPasskeyOpt{ + internalContext: registrationFixtureSuccessInternalContext, + } +} + +type submitPasskeyOption func(o *submitPasskeyOpt) + +func withUserID(id string) submitPasskeyOption { + return func(o *submitPasskeyOpt) { + o.userID = base64.StdEncoding.EncodeToString([]byte(id)) + } +} + +func withInternalContext(ic sqlxx.JSONRawMessage) submitPasskeyOption { + return func(o *submitPasskeyOpt) { + o.internalContext = ic + } +} + +func (fix *fixture) submitPasskeyRegistration( + t *testing.T, + flowType string, + client *http.Client, + cb func(values url.Values), + opts ...submitPasskeyOption, +) (string, *http.Response, *kratos.RegistrationFlow) { + o := newSubmitPasskeyOpt() + for _, fn := range opts { + fn(o) + } + + isSPA := flowType == "spa" + regFlow := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, isSPA, false, false, o.initFlowOpts...) + + // First step: fill out traits and click on "sign up with passkey" + values := testhelpers.SDKFormFieldsToURLValues(regFlow.Ui.Nodes) + cb(values) + passkeyRegisterVal := values.Get(node.PasskeyRegister) // needed in the second step + values.Del(node.PasskeyRegister) + values.Set("method", "passkey") + body, _ := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, values.Encode()) + + // We inject the session to replay + interim, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(fix.ctx, uuid.FromStringOrNil(regFlow.Id)) + require.NoError(t, err) + interim.InternalContext = o.internalContext + if o.userID != "" { + interim.InternalContext, err = sjson.SetBytes(interim.InternalContext, "passkey_session_data.user_id", o.userID) + require.NoError(t, err) + } + require.NoError(t, fix.reg.RegistrationFlowPersister().UpdateRegistrationFlow(fix.ctx, interim)) + + // Second step: fill out passkey response + values.Set(node.PasskeyRegister, passkeyRegisterVal) + body, res := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, values.Encode()) + + return body, res, regFlow +} + +func (fix *fixture) makeRegistration(t *testing.T, flowType string, values func(v url.Values), opts ...submitPasskeyOption) (actual string, res *http.Response, fetchedFlow *registration.Flow) { + actual, res, actualFlow := fix.submitPasskeyRegistration(t, flowType, testhelpers.NewClientWithCookies(t), values, opts...) + fetchedFlow, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(fix.ctx, uuid.FromStringOrNil(actualFlow.Id)) + require.NoError(t, err) + + return actual, res, fetchedFlow +} + +func (fix *fixture) makeSuccessfulRegistration(t *testing.T, flowType string, expectReturnTo string, values func(v url.Values), opts ...submitPasskeyOption) (actual string) { + actual, res, _ := fix.makeRegistration(t, flowType, values, opts...) + if flowType == "spa" { + expectReturnTo = fix.publicTS.URL + } + assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual)) + return actual +} + +func (fix *fixture) createIdentityWithoutPasskey(t *testing.T) *identity.Identity { + id := fix.createIdentity(t) + delete(id.Credentials, identity.CredentialsTypePasskey) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id)) + return id +} + +func (fix *fixture) createIdentityAndReturnIdentifier(t *testing.T, conf []byte) (*identity.Identity, string) { + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + p, err := fix.reg.Hasher(fix.ctx).Generate(fix.ctx, []byte(password)) + require.NoError(t, err) + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: identifier, + Verified: false, + CreatedAt: time.Now(), + }, + }, + } + if conf == nil { + conf = []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo"},{"id":"YmFyYmFy","display_name":"bar"}]}`) + } + require.NoError(t, fix.reg.PrivilegedIdentityPool().CreateIdentity(fix.ctx, i)) + i.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + identity.CredentialsTypePasskey: { + Type: identity.CredentialsTypePasskey, + Identifiers: []string{identifier}, + Config: conf, + }, + } + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, i)) + return i, identifier +} + +func (fix *fixture) createIdentity(t *testing.T) *identity.Identity { + id, _ := fix.createIdentityAndReturnIdentifier(t, nil) + return id +} diff --git a/selfservice/strategy/password/op_registration_test.go b/selfservice/strategy/password/op_registration_test.go index 7c0b05c81e5a..40b3d0193511 100644 --- a/selfservice/strategy/password/op_registration_test.go +++ b/selfservice/strategy/password/op_registration_test.go @@ -34,6 +34,7 @@ import ( func TestOAuth2ProviderRegistration(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) kratosPublicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, x.NewRouterPublic(), x.NewRouterAdmin()) errTS := testhelpers.NewErrorTestServer(t, reg) diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 32b090cf1183..55970fdf0b5e 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -53,7 +53,7 @@ type UpdateRegistrationFlowWithPasswordMethod struct { TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) RegisterRegistrationRoutes(_ *x.RouterPublic) { +func (s *Strategy) RegisterRegistrationRoutes(*x.RouterPublic) { } func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, f *registration.Flow, p *UpdateRegistrationFlowWithPasswordMethod, err error) error { diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index ddcf26a20883..14bf2382b212 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -45,6 +45,8 @@ func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) + return reg } @@ -54,6 +56,7 @@ func TestRegistration(t *testing.T) { t.Run("case=registration", func(t *testing.T) { reg := newRegistrationRegistry(t) conf := reg.Config() + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) router := x.NewRouterPublic() admin := x.NewRouterAdmin() @@ -649,6 +652,7 @@ func TestRegistration(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/") + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) testhelpers.SetDefaultIdentitySchema(conf, "file://stub/sort.schema.json") conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) diff --git a/selfservice/strategy/profile/.schema/registration.schema.json b/selfservice/strategy/profile/.schema/registration.schema.json new file mode 100644 index 000000000000..4a4d5dba16c4 --- /dev/null +++ b/selfservice/strategy/profile/.schema/registration.schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "traits": { + "description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect." + }, + "screen": { + "type": "string", + "enum": [ + "credential-selection" + ] + }, + "method": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go index 669d4c0e8d5d..0942d738840e 100644 --- a/selfservice/strategy/profile/strategy.go +++ b/selfservice/strategy/profile/strategy.go @@ -10,7 +10,7 @@ import ( "time" "github.com/ory/jsonschema/v3" - + "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/text" "github.com/gofrs/uuid" @@ -60,6 +60,8 @@ type ( settings.StrategyProvider settings.HooksProvider + registration.FlowPersistenceProvider + schema.IdentityTraitsProvider } Strategy struct { diff --git a/selfservice/strategy/profile/two_step_registration.go b/selfservice/strategy/profile/two_step_registration.go new file mode 100644 index 000000000000..98f4fe806913 --- /dev/null +++ b/selfservice/strategy/profile/two_step_registration.go @@ -0,0 +1,239 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile + +import ( + _ "embed" + "encoding/json" + "net/http" + + "github.com/tidwall/gjson" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +//go:embed .schema/registration.schema.json +var registrationSchema []byte + +var _ registration.Strategy = new(Strategy) + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeProfile +} + +func (s *Strategy) RegisterRegistrationRoutes(*x.RouterPublic) {} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(r.Context()) { + return nil + } + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil) + if err != nil { + return err + } + + for _, n := range nodes { + f.UI.SetNode(n) + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.Append(node.NewInputField( + "method", + "profile", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistration())) + + return nil +} + +// Update Registration Flow with Profile Method +// +// swagger:model updateRegistrationFlowWithProfileMethod +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateRegistrationFlowWithProfileMethod struct { + // Traits + // + // The identity's traits. + // + // required: true + Traits json.RawMessage `json:"traits"` + + // Method + // + // Should be set to profile when trying to update a profile. + // + // required: true + Method string `json:"method"` + + // Screen requests navigation to a previous screen. + // + // This must be set to credential-selection to go back to the credential + // selection screen. + // + // required: false + Screen string `json:"screen" form:"screen"` + + // FlowIDRequestID is the flow ID. + // + // swagger:ignore + FlowID string `json:"flow"` + + // The Anti-CSRF Token + // + // This token is only required when performing browser flows. + CSRFToken string `json:"csrf_token"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty"` +} + +func (s *Strategy) decode(p *updateRegistrationFlowWithProfileMethod, r *http.Request) error { + return registration.DecodeBody(p, r, s.dc, s.d.Config(), registrationSchema) +} + +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) { + if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(r.Context()) { + return flow.ErrStrategyNotResponsible + } + + var params updateRegistrationFlowWithProfileMethod + + if err = s.decode(¶ms, r); err != nil { + return s.handleRegistrationError(w, r, regFlow, ¶ms, err) + } + + if params.Screen == "credential-selection" { + params.Method = "profile" + } + + switch params.Method { + case "profile": + return s.displayStepTwoNodes(w, r, regFlow, i, params) + case "profile:back": + return s.displayStepOneNodes(w, r, regFlow, i, params) + } + // Default case + return flow.ErrStrategyNotResponsible +} + +func (s *Strategy) displayStepOneNodes(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, _ *identity.Identity, params updateRegistrationFlowWithProfileMethod) error { + ctx := r.Context() + regFlow.UI.ResetMessages() + err := json.Unmarshal([]byte(gjson.GetBytes(regFlow.InternalContext, "stepOneNodes").Raw), ®Flow.UI.Nodes) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, ¶ms, err) + } + regFlow.UI.UpdateNodeValuesFromJSON(params.Traits, "traits", node.DefaultGroup) + + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(w, r, regFlow, ¶ms, err) + } + + redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String() + if x.IsJSONRequest(r) { + s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow) + } else { + http.Redirect(w, r, redirectTo, http.StatusSeeOther) + } + + return flow.ErrCompletedByStrategy +} + +func (s *Strategy) displayStepTwoNodes(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity, params updateRegistrationFlowWithProfileMethod) error { + ctx := r.Context() + + // Reset state-esque flow fields + regFlow.Active = "" + regFlow.State = "choose_method" + + regFlow.UI.ResetMessages() + regFlow.TransientPayload = params.TransientPayload + + if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, params.CSRFToken); err != nil { + return s.handleRegistrationError(w, r, regFlow, ¶ms, err) + } + + if len(params.Traits) == 0 { + params.Traits = json.RawMessage("{}") + } + i.Traits = identity.Traits(params.Traits) + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { + return s.handleRegistrationError(w, r, regFlow, ¶ms, err) + } + + err := json.Unmarshal([]byte(gjson.GetBytes(regFlow.InternalContext, "stepTwoNodes").Raw), ®Flow.UI.Nodes) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, ¶ms, err) + } + + regFlow.UI.Messages.Add(text.NewInfoSelfServiceChooseCredentials()) + + regFlow.UI.Nodes.Append(node.NewInputField( + "method", + "profile:back", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistrationBack())) + + regFlow.UI.UpdateNodeValuesFromJSON(json.RawMessage(i.Traits), "traits", node.DefaultGroup) + for _, n := range regFlow.UI.Nodes { + if n.Group != node.DefaultGroup || n.Type != node.Input { + continue + } + if attr, ok := n.Attributes.(*node.InputAttributes); ok { + attr.Type = node.InputAttributeTypeHidden + } + } + + if regFlow.Type == flow.TypeBrowser { + regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + + if err = s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(w, r, regFlow, ¶ms, err) + } + + redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String() + if x.IsJSONRequest(r) { + s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow) + } else { + http.Redirect(w, r, redirectTo, http.StatusSeeOther) + } + + return flow.ErrCompletedByStrategy +} + +func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, regFlow *registration.Flow, params *updateRegistrationFlowWithProfileMethod, err error) error { + if regFlow != nil { + if params != nil { + for _, n := range container.NewFromJSON("", node.ProfileGroup, params.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + regFlow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } + + if regFlow.Type == flow.TypeBrowser { + regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index 0e2e343e00e0..ca960c98d683 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -61,7 +61,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index e6376bbf7d9e..f4be195cdecf 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index e6376bbf7d9e..f4be195cdecf 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json index 2405a57aaf04..6668b171ed43 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json @@ -14,6 +14,7 @@ }, { "attributes": { + "autocomplete": "username webauthn", "disabled": false, "name": "identifier", "node_type": "input", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index 8b27f6ca0daf..581bff275b17 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index d27075b6063d..0b1702c09413 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -116,7 +116,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json index ab452afa7ea9..2fdeed6db8b4 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json @@ -1,10 +1,11 @@ { "type": "input", - "group": "default", + "group": "webauthn", "attributes": { "name": "webauthn_remove", - "type": "text", - "disabled": false, + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, "node_type": "input" }, "messages": [ @@ -17,5 +18,16 @@ } } ], - "meta": {} + "meta": { + "label": { + "id": 1050018, + "text": "Remove security key \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json index 0a9fb6164387..515658a3d64f 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json @@ -7,5 +7,8 @@ ], "webauthn_register_trigger": [ "" + ], + "webauthn_remove": [ + "666f6f666f6f" ] } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json index ab452afa7ea9..2fdeed6db8b4 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json @@ -1,10 +1,11 @@ { "type": "input", - "group": "default", + "group": "webauthn", "attributes": { "name": "webauthn_remove", - "type": "text", - "disabled": false, + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, "node_type": "input" }, "messages": [ @@ -17,5 +18,16 @@ } } ], - "meta": {} + "meta": { + "label": { + "id": 1050018, + "text": "Remove security key \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json index 0a9fb6164387..515658a3d64f 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json @@ -7,5 +7,8 @@ ], "webauthn_register_trigger": [ "" + ], + "webauthn_remove": [ + "666f6f666f6f" ] } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index ab20337abc14..b21fa4833028 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -68,7 +68,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index f95c0ade36aa..14a920d0a18d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index f95c0ade36aa..14a920d0a18d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_missing_user_id.json b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_missing_user_id.json new file mode 100644 index 000000000000..1f4913b5375e --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_missing_user_id.json @@ -0,0 +1,7 @@ +{ + "webauthn_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "", + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_wrong_user_id.json b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_wrong_user_id.json new file mode 100644 index 000000000000..6eaa968b6f56 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_wrong_user_id.json @@ -0,0 +1,7 @@ +{ + "webauthn_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "wrong", + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/js/webauthn.js b/selfservice/strategy/webauthn/js/webauthn.js deleted file mode 100644 index 44c389d6f16b..000000000000 --- a/selfservice/strategy/webauthn/js/webauthn.js +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -;(function () { - if (!window) { - return - } - - if (!window.PublicKeyCredential) { - alert("This browser does not support WebAuthn!") - } - - if (window.__oryWebAuthnInitialized) { - return - } - - function __oryWebAuthnBufferDecode(value) { - return Uint8Array.from( - atob(value.replaceAll("-", "+").replaceAll("_", "/")), - function (c) { - return c.charCodeAt(0) - }, - ) - } - - function __oryWebAuthnBufferEncode(value) { - return btoa(String.fromCharCode.apply(null, new Uint8Array(value))) - .replaceAll("+", "-") - .replaceAll("/", "_") - .replaceAll("=", "") - } - - function __oryWebAuthnLogin( - opt, - resultQuerySelector = '*[name="webauthn_login"]', - triggerQuerySelector = '*[name="webauthn_login_trigger"]', - ) { - if (!window.PublicKeyCredential) { - alert("This browser does not support WebAuthn!") - } - - opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) - opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( - function (value) { - return { - ...value, - id: __oryWebAuthnBufferDecode(value.id), - } - }, - ) - - navigator.credentials - .get(opt) - .then(function (credential) { - document.querySelector(resultQuerySelector).value = JSON.stringify({ - id: credential.id, - rawId: __oryWebAuthnBufferEncode(credential.rawId), - type: credential.type, - response: { - authenticatorData: __oryWebAuthnBufferEncode( - credential.response.authenticatorData, - ), - clientDataJSON: __oryWebAuthnBufferEncode( - credential.response.clientDataJSON, - ), - signature: __oryWebAuthnBufferEncode(credential.response.signature), - userHandle: __oryWebAuthnBufferEncode( - credential.response.userHandle, - ), - }, - }) - - document.querySelector(triggerQuerySelector).closest("form").submit() - }) - .catch((err) => { - alert(err) - }) - } - - function __oryWebAuthnRegistration( - opt, - resultQuerySelector = '*[name="webauthn_register"]', - triggerQuerySelector = '*[name="webauthn_register_trigger"]', - ) { - if (!window.PublicKeyCredential) { - alert("This browser does not support WebAuthn!") - } - - opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) - opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) - - if (opt.publicKey.excludeCredentials) { - opt.publicKey.excludeCredentials = opt.publicKey.excludeCredentials.map( - function (value) { - return { - ...value, - id: __oryWebAuthnBufferDecode(value.id), - } - }, - ) - } - - navigator.credentials - .create(opt) - .then(function (credential) { - document.querySelector(resultQuerySelector).value = JSON.stringify({ - id: credential.id, - rawId: __oryWebAuthnBufferEncode(credential.rawId), - type: credential.type, - response: { - attestationObject: __oryWebAuthnBufferEncode( - credential.response.attestationObject, - ), - clientDataJSON: __oryWebAuthnBufferEncode( - credential.response.clientDataJSON, - ), - }, - }) - - document.querySelector(triggerQuerySelector).closest("form").submit() - }) - .catch((err) => { - alert(err) - }) - } - - window["__oryWebAuthnLogin"] = __oryWebAuthnLogin - window["__oryWebAuthnRegistration"] = __oryWebAuthnRegistration - window["__oryWebAuthnInitialized"] = true -})() diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 6f160929c124..4c7dd23f09ea 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -11,6 +11,7 @@ import ( "github.com/ory/kratos/selfservice/flowhelpers" "github.com/ory/kratos/session" + "github.com/ory/kratos/x/webauthnx" "github.com/gofrs/uuid" @@ -18,8 +19,6 @@ import ( "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" - "github.com/ory/x/urlx" - "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/tidwall/gjson" @@ -35,20 +34,24 @@ import ( "github.com/ory/x/decoderx" ) +func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { + webauthnx.RegisterWebauthnRoute(r) +} + func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { if sr.Type != flow.TypeBrowser { return nil } if s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel1) { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, ErrNoCredentials) { + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { return nil } else if err != nil { return err } return nil } else if sr.IsForced() { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, ErrNoCredentials) { + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { return nil } else if err != nil { return err @@ -61,7 +64,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au return err } - if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, ErrNoCredentials) { + if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) { return nil } else if err != nil { return err @@ -80,7 +83,7 @@ func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login return nil } - if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, ErrNoCredentials) { + if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, webauthnx.ErrNoCredentials) { return nil } else if err != nil { return err @@ -101,7 +104,14 @@ func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + sr.UI.SetNode(node.NewInputField( + "identifier", + "", + node.DefaultGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute, + func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, + ).WithMetaLabel(identifierLabel)) sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn())) return nil } @@ -130,7 +140,7 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident if len(webAuthCreds) == 0 { // Identity has no webauthn - return ErrNoCredentials + return webauthnx.ErrNoCredentials } web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) @@ -138,7 +148,7 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate WebAuth.").WithDebug(err.Error())) } - options, sessionData, err := web.BeginLogin(NewUser(conf.UserHandle, webAuthCreds, web.Config)) + options, sessionData, err := web.BeginLogin(webauthnx.NewUser(conf.UserHandle, webAuthCreds, web.Config)) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate WebAuth login.").WithDebug(err.Error())) } @@ -159,10 +169,10 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.Nodes.Upsert(NewWebAuthnScript(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), webAuthnRoute).String(), jsOnLoad)) - sr.UI.SetNode(NewWebAuthnLoginTrigger(string(injectWebAuthnOptions)). + sr.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(r.Context()))) + sr.UI.SetNode(webauthnx.NewWebAuthnLoginTrigger(string(injectWebAuthnOptions)). WithMetaLabel(label)) - sr.UI.Nodes.Upsert(NewWebAuthnLoginInput()) + sr.UI.Nodes.Upsert(webauthnx.NewWebAuthnLoginInput()) return nil } @@ -267,7 +277,7 @@ func (s *Strategy) loginPasswordless(w http.ResponseWriter, r *http.Request, f * previousNodes := f.UI.Nodes f.UI.Nodes = node.Nodes{} - if err := s.populateLoginMethod(r, f, i, text.NewInfoSelfServiceLoginContinue(), identity.AuthenticatorAssuranceLevel1); errors.Is(err, ErrNoCredentials) { + if err := s.populateLoginMethod(r, f, i, text.NewInfoSelfServiceLoginContinue(), identity.AuthenticatorAssuranceLevel1); errors.Is(err, webauthnx.ErrNoCredentials) { f.UI.Nodes = previousNodes return nil, s.handleLoginError(r, f, schema.NewNoWebAuthnCredentials()) } else if err != nil { @@ -331,7 +341,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * webAuthCreds = o.Credentials.ToWebAuthn() } - if _, err := web.ValidateLogin(NewUser(o.UserHandle, webAuthCreds, web.Config), webAuthnSess, webAuthnResponse); err != nil { + if _, err := web.ValidateLogin(webauthnx.NewUser(o.UserHandle, webAuthCreds, web.Config), webAuthnSess, webAuthnResponse); err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/"))) } diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go index 7ca2b3a67301..85cb3628e59d 100644 --- a/selfservice/strategy/webauthn/registration.go +++ b/selfservice/strategy/webauthn/registration.go @@ -7,7 +7,6 @@ import ( "encoding/json" "net/http" "strings" - "time" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -23,7 +22,7 @@ import ( "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" - "github.com/ory/x/urlx" + "github.com/ory/kratos/x/webauthnx" ) // Update Registration Flow with WebAuthn Method @@ -92,20 +91,22 @@ func (s *Strategy) decode(p *updateRegistrationFlowWithWebAuthnMethod, r *http.R return registration.DecodeBody(p, r, s.hd, s.d.Config(), registrationSchema) } -func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { - if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) { + ctx := r.Context() + + if regFlow.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { return flow.ErrStrategyNotResponsible } var p updateRegistrationFlowWithWebAuthnMethod if err := s.decode(&p, r); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(w, r, regFlow, &p, err) } - f.TransientPayload = p.TransientPayload + regFlow.TransientPayload = p.TransientPayload - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return s.handleRegistrationError(w, r, regFlow, &p, err) } if len(p.Register) == 0 { @@ -113,8 +114,8 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := flow.MethodEnabledAndAllowed(ctx, regFlow.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return s.handleRegistrationError(w, r, regFlow, &p, err) } if len(p.Traits) == 0 { @@ -122,67 +123,72 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } i.Traits = identity.Traits(p.Traits) - webAuthnSession := gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + webAuthnSession := gjson.GetBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) if !webAuthnSession.IsObject() { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object."))) + return s.handleRegistrationError(w, r, regFlow, &p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object."))) } var webAuthnSess webauthn.SessionData - if err := json.Unmarshal([]byte(gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)).Raw), &webAuthnSess); err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) + if err := json.Unmarshal([]byte(webAuthnSession.Raw), &webAuthnSess); err != nil { + return s.handleRegistrationError(w, r, regFlow, &p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) } webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(p.Register)) if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err))) + return s.handleRegistrationError(w, r, regFlow, &p, errors.WithStack( + herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err))) } web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) + return s.handleRegistrationError(w, r, regFlow, &p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) } - credential, err := web.CreateCredential(NewUser(webAuthnSess.UserID, nil, web.Config), webAuthnSess, webAuthnResponse) + credential, err := web.CreateCredential(webauthnx.NewUser(webAuthnSess.UserID, nil, web.Config), webAuthnSess, webAuthnResponse) if err != nil { if devErr := new(protocol.Error); errors.As(err, &devErr) { s.d.Logger().WithError(err).WithField("error_devinfo", devErr.DevInfo).Error("Failed to create WebAuthn credential") } - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err))) + return s.handleRegistrationError(w, r, regFlow, &p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err))) } - var cc identity.CredentialsWebAuthnConfig - wc := identity.CredentialFromWebAuthn(credential, true) - wc.AddedAt = time.Now().UTC().Round(time.Second) - wc.DisplayName = p.RegisterDisplayName - wc.IsPasswordless = s.d.Config().WebAuthnForPasswordless(r.Context()) - cc.UserHandle = webAuthnSess.UserID - - cc.Credentials = append(cc.Credentials, *wc) - co, err := json.Marshal(cc) + credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true) + credentialWebAuthn.DisplayName = p.RegisterDisplayName + credentialWebAuthnConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{ + Credentials: identity.CredentialsWebAuthn{*credentialWebAuthn}, + UserHandle: webAuthnSess.UserID, + }) if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))) + return s.handleRegistrationError(w, r, regFlow, &p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))) } - i.UpsertCredentialsConfig(s.ID(), co, 1) - if err := s.validateCredentials(r.Context(), i); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + i.UpsertCredentialsConfig(s.ID(), credentialWebAuthnConfig, 1) + if err := s.validateCredentials(ctx, i); err != nil { + return s.handleRegistrationError(w, r, regFlow, &p, err) } // Remove the WebAuthn URL from the internal context now that it is set! - f.InternalContext, err = sjson.DeleteBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + regFlow.InternalContext, err = sjson.DeleteBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) if err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(w, r, regFlow, &p, err) } - if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(r.Context(), f); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(w, r, regFlow, &p, err) } return nil } func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { - if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { + ctx := r.Context() + + if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) { return nil } @@ -191,7 +197,7 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.F return err } - nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil) + nodes, err := container.NodesFromJSONSchema(ctx, node.DefaultGroup, ds.String(), "", nil) if err != nil { return err } @@ -200,13 +206,13 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.F f.UI.SetNode(n) } - web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) if err != nil { return errors.WithStack(err) } webauthID := x.NewUUID() - user := NewUser(webauthID[:], nil, s.d.Config().WebAuthnConfig(r.Context())) + user := webauthnx.NewUser(webauthID[:], nil, s.d.Config().WebAuthnConfig(ctx)) option, sessionData, err := web.BeginRegistration(user) if err != nil { return errors.WithStack(err) @@ -222,10 +228,10 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.F return errors.WithStack(err) } - f.UI.Nodes.Upsert(NewWebAuthnScript(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), webAuthnRoute).String(), jsOnLoad)) - f.UI.Nodes.Upsert(NewWebAuthnConnectionName()) - f.UI.Nodes.Upsert(NewWebAuthnConnectionInput()) - f.UI.Nodes.Upsert(NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionName()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionInput()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterWebAuthn())) f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index cc20d431230c..035e7ffd984c 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -40,6 +40,8 @@ var ( registrationFixtureSuccessResponse []byte //go:embed fixtures/registration/success/internal_context.json registrationFixtureSuccessInternalContext []byte + //go:embed fixtures/registration/failure/internal_context_wrong_user_id.json + registrationFixtureFailureInternalContextWrongUserID []byte ) func flowToIsSPA(flow string) bool { @@ -52,6 +54,8 @@ func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { enableWebAuthn(conf) conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) + return reg } @@ -236,6 +240,47 @@ func TestRegistration(t *testing.T) { return body, res, f } + t.Run("case=should return an error because internal context is invalid", func(t *testing.T) { + email := testhelpers.RandomEmail() + + for _, tc := range []struct { + name string + internalContext string + }{{ + name: "invalid json", + internalContext: "invalid", + }, { + name: "wrong user ID", + internalContext: string(registrationFixtureFailureInternalContextWrongUserID), + }} { + tc := tc + t.Run("context="+tc.name, func(t *testing.T) { + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := submitWebAuthnRegistrationWithClient(t, f, + []byte(tc.internalContext), + testhelpers.NewClientWithCookies(t), + values, + ) + + if f == "spa" { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "error.status").String(), "%s", actual) + } else { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual) + } + }) + } + }) + } + }) + t.Run("case=should fail to create identity if schema is missing the identifier", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/noid.schema.json") t.Cleanup(func() { diff --git a/selfservice/strategy/webauthn/settings.go b/selfservice/strategy/webauthn/settings.go index adbb91e8ae8c..3626b3fbe61c 100644 --- a/selfservice/strategy/webauthn/settings.go +++ b/selfservice/strategy/webauthn/settings.go @@ -11,8 +11,8 @@ import ( "time" "github.com/ory/kratos/text" - - "github.com/ory/x/urlx" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/webauthnx" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -167,7 +167,7 @@ func (s *Strategy) continueSettingsFlow( } if len(p.Register) > 0 { - return s.continueSettingsFlowAdd(w, r, ctxUpdate, p) + return s.continueSettingsFlowAdd(r, ctxUpdate, p) } else if len(p.Remove) > 0 { return s.continueSettingsFlowRemove(w, r, ctxUpdate, p) } @@ -211,7 +211,7 @@ func (s *Strategy) continueSettingsFlowRemove(w http.ResponseWriter, r *http.Req } if count < 2 && wasPasswordless { - return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(ErrNotEnoughCredentials)) + return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(webauthnx.ErrNotEnoughCredentials)) } if len(updated) == 0 { @@ -231,7 +231,7 @@ func (s *Strategy) continueSettingsFlowRemove(w http.ResponseWriter, r *http.Req return nil } -func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod) error { +func (s *Strategy) continueSettingsFlowAdd(r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod) error { webAuthnSession := gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) if !webAuthnSession.IsObject() { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object.")) @@ -252,7 +252,7 @@ func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Reques return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())) } - credential, err := web.CreateCredential(NewUser(ctxUpdate.Session.IdentityID[:], nil, web.Config), webAuthnSess, webAuthnResponse) + credential, err := web.CreateCredential(webauthnx.NewUser(ctxUpdate.Session.IdentityID[:], nil, web.Config), webAuthnSess, webAuthnResponse) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err)) } @@ -353,11 +353,10 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity // We only show the option to remove a credential, if it is not the last one when passwordless, // or, if it is for MFA we show it always. cred := &webAuthns.Credentials[k] - if cred.IsPasswordless && count < 2 { + f.UI.Nodes.Append(webauthnx.NewWebAuthnUnlink(cred, func(a *node.InputAttributes) { // Do not remove this node because it is the last credential the identity can sign in with. - continue - } - f.UI.Nodes.Append(NewWebAuthnUnlink(cred)) + a.Disabled = cred.IsPasswordless && count < 2 + })) } } @@ -366,7 +365,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity return errors.WithStack(err) } - option, sessionData, err := web.BeginRegistration(NewUser(id.ID.Bytes(), nil, web.Config)) + option, sessionData, err := web.BeginRegistration(webauthnx.NewUser(id.ID.Bytes(), nil, web.Config)) if err != nil { return errors.WithStack(err) } @@ -381,11 +380,11 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity return errors.WithStack(err) } - f.UI.Nodes.Upsert(NewWebAuthnScript(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), webAuthnRoute).String(), jsOnLoad)) - f.UI.Nodes.Upsert(NewWebAuthnConnectionName()) - f.UI.Nodes.Upsert(NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(r.Context()))) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionName()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterWebAuthn())) - f.UI.Nodes.Upsert(NewWebAuthnConnectionInput()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionInput()) return nil } diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index 27413df38b16..acf4fd357b1d 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -100,10 +100,11 @@ func createIdentity(t *testing.T, reg driver.Registry) *identity.Identity { } func enableWebAuthn(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.display_name", "Ory Corp") - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.id", "localhost") - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.origin", "http://localhost:4455") + webauthn := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypeWebAuthn) + conf.MustSet(ctx, webauthn+".enabled", true) + conf.MustSet(ctx, webauthn+".config.rp.display_name", "Ory Corp") + conf.MustSet(ctx, webauthn+".config.rp.id", "localhost") + conf.MustSet(ctx, webauthn+".config.rp.origin", "http://localhost:4455") } func ensureReplacement(t *testing.T, index string, ui kratos.UiContainer, expected string) { diff --git a/selfservice/strategy/webauthn/user.go b/selfservice/strategy/webauthn/user.go deleted file mode 100644 index 823ca93de106..000000000000 --- a/selfservice/strategy/webauthn/user.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package webauthn - -import "github.com/go-webauthn/webauthn/webauthn" - -var _ webauthn.User = (*User)(nil) - -type User struct { - id []byte - c []webauthn.Credential - cfg *webauthn.Config -} - -func NewUser(id []byte, c []webauthn.Credential, cfg *webauthn.Config) *User { - return &User{ - id: id, - c: c, - cfg: cfg, - } -} - -func (u *User) WebAuthnID() []byte { - return u.id -} - -func (u *User) WebAuthnName() string { - return u.cfg.RPDisplayName -} - -func (u *User) WebAuthnDisplayName() string { - return u.cfg.RPDisplayName -} - -func (u *User) WebAuthnIcon() string { - return "" // Icon option has been removed due to security considerations. -} - -func (u *User) WebAuthnCredentials() []webauthn.Credential { - return u.c -} diff --git a/spec/api.json b/spec/api.json index ecb4debbb17d..2910ad7c6999 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2151,7 +2151,7 @@ "$ref": "#/components/schemas/uiNodeAttributes" }, "group": { - "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup", + "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup", "enum": [ "default", "password", @@ -2161,10 +2161,11 @@ "code", "totp", "lookup_secret", - "webauthn" + "webauthn", + "passkey" ], "type": "string", - "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup" + "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup" }, "messages": { "$ref": "#/components/schemas/uiTexts" @@ -2322,6 +2323,10 @@ "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", "type": "string" }, + "onload": { + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.", + "type": "string" + }, "pattern": { "description": "The input's pattern.", "type": "string" @@ -2661,6 +2666,27 @@ ], "type": "object" }, + "updateLoginFlowWithPasskeyMethod": { + "description": "Update Login Flow with Passkey Method", + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"passkey\" when logging in using the Passkey strategy.", + "type": "string" + }, + "passkey_login": { + "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, "updateLoginFlowWithPasswordMethod": { "description": "Update Login Flow with Password Method", "properties": { @@ -2937,6 +2963,36 @@ ], "type": "object" }, + "updateRegistrationFlowWithPasskeyMethod": { + "description": "Update Registration Flow with Passkey Method", + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a Passkey.", + "type": "string" + }, + "passkey_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + }, + "required": [ + "traits", + "method" + ], + "type": "object" + }, "updateRegistrationFlowWithPasswordMethod": { "description": "Update Registration Flow with Password Method", "properties": { @@ -3113,6 +3169,31 @@ ], "type": "object" }, + "updateSettingsFlowWithPasskeyMethod": { + "description": "Update Settings Flow with Passkey Method", + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing.", + "type": "string" + }, + "passkey_remove": { + "description": "Remove a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + }, + "passkey_settings_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, "updateSettingsFlowWithPasswordMethod": { "description": "Update Settings Flow with Password Method", "properties": { diff --git a/spec/swagger.json b/spec/swagger.json index 0f99a63b0ee7..df1ddfe4a6b4 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5249,7 +5249,7 @@ "$ref": "#/definitions/uiNodeAttributes" }, "group": { - "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup", + "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup", "type": "string", "enum": [ "default", @@ -5260,9 +5260,10 @@ "code", "totp", "lookup_secret", - "webauthn" + "webauthn", + "passkey" ], - "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup" + "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup" }, "messages": { "$ref": "#/definitions/uiTexts" @@ -5392,6 +5393,10 @@ "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", "type": "string" }, + "onload": { + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.", + "type": "string" + }, "pattern": { "description": "The input's pattern.", "type": "string" @@ -5695,6 +5700,27 @@ } } }, + "updateLoginFlowWithPasskeyMethod": { + "description": "Update Login Flow with Passkey Method", + "type": "object", + "required": [ + "method" + ], + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"passkey\" when logging in using the Passkey strategy.", + "type": "string" + }, + "passkey_login": { + "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + } + } + }, "updateLoginFlowWithPasswordMethod": { "description": "Update Login Flow with Password Method", "type": "object", @@ -5935,6 +5961,36 @@ } } }, + "updateRegistrationFlowWithPasskeyMethod": { + "description": "Update Registration Flow with Passkey Method", + "type": "object", + "required": [ + "traits", + "method" + ], + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a Passkey.", + "type": "string" + }, + "passkey_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, "updateRegistrationFlowWithPasswordMethod": { "description": "Update Registration Flow with Password Method", "type": "object", @@ -6078,6 +6134,31 @@ } } }, + "updateSettingsFlowWithPasskeyMethod": { + "description": "Update Settings Flow with Passkey Method", + "type": "object", + "required": [ + "method" + ], + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing.", + "type": "string" + }, + "passkey_remove": { + "description": "Remove a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + }, + "passkey_settings_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + } + } + }, "updateSettingsFlowWithPasswordMethod": { "description": "Update Settings Flow with Password Method", "type": "object", diff --git a/test/e2e/cypress.config.ts b/test/e2e/cypress.config.ts index 45ddc9bbd0c3..828b10290695 100644 --- a/test/e2e/cypress.config.ts +++ b/test/e2e/cypress.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ runMode: 6, openMode: 1, }, + experimentalRunAllSpecs: true, videosFolder: "cypress/videos", screenshotsFolder: "cypress/screenshots", excludeSpecPattern: ["**/*snapshots.js", "playwright/**"], diff --git a/test/e2e/cypress/helpers/express.ts b/test/e2e/cypress/helpers/express.ts index 138cf433fc5b..4631edc34426 100644 --- a/test/e2e/cypress/helpers/express.ts +++ b/test/e2e/cypress/helpers/express.ts @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { APP_URL, SPA_URL } from "./index" +import { APP_URL } from "./index" export const routes = { base: APP_URL, diff --git a/test/e2e/cypress/helpers/index.ts b/test/e2e/cypress/helpers/index.ts index 1fae8d176adb..52bcb339ad50 100644 --- a/test/e2e/cypress/helpers/index.ts +++ b/test/e2e/cypress/helpers/index.ts @@ -37,7 +37,7 @@ export const assertRecoveryAddress = expect(address.value).to.equal(email) } -export const parseHtml = (html) => +export const parseHtml = (html: string) => new DOMParser().parseFromString(html, "text/html") export const APP_URL = ( diff --git a/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts new file mode 100644 index 000000000000..95fafafa0f30 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts @@ -0,0 +1,298 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../helpers" +import { routes as express } from "../../../helpers/express" +import { routes as react } from "../../../helpers/react" +import { testRegistrationWebhook } from "../../../helpers/webhook" + +const signup = (registration: string, app: string, email = gen.email()) => { + cy.visit(registration) + + const websiteTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.website"]` + const emailTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.email"]` + + cy.get(emailTrait).type(email) + cy.get(websiteTrait).type("https://www.ory.sh") + cy.get('[name="passkey_register_trigger"]').click() + + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) +} + +context("Passkey registration", () => { + before(() => { + cy.task("resetCRI", {}) + }) + after(() => { + cy.task("resetCRI", {}) + }) + ;[ + { + login: react.login, + registration: express.registration, + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "passkey", + }, + { + login: express.login, + registration: express.registration, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "passkey", + }, + ].forEach(({ registration, login, profile, app, base, settings }) => { + describe(`for app ${app}`, () => { + let authenticator: any + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + + cy.task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }) + .then(() => { + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "internal", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }) + }) + .then((result) => { + authenticator = result + cy.log("authenticator ID:", authenticator) + }) + + cy.longPrivilegedSessionTime() + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.task("sendCRI", { + query: "WebAuthn.clearCredentials", + opts: authenticator, + }) + }) + + after(() => { + cy.task("sendCRI", { + query: "WebAuthn.removeVirtualAuthenticator", + opts: authenticator, + }) + }) + + it("should register after validation errors", () => { + cy.visit(registration) + + // the browser will prevent the form from being submitted if the input field is required + // we should remove the required attribute to simulate the data not being sent + cy.removeAttribute( + ['input[name="traits.email"]', 'input[name="traits.website"]'], + "required", + ) + + cy.get(`input[name="traits.website"]`).then(($el) => { + $el.removeAttr("type") + }) + + const websiteTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.website"]` + + const emailTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.email"]` + + cy.get(websiteTrait).type("b") + cy.get('[name="passkey_register_trigger"]').click() + + cy.get('[data-testid="ui/message/4000002"]').should("to.exist") + cy.get('[data-testid="ui/message/4000001"]').should("to.exist") + cy.get(websiteTrait).should("have.value", "b") + + const email = gen.email() + cy.get(emailTrait).type(email) + cy.get('[name="passkey_register_trigger"]').click() + + cy.wait(1000) + + cy.get('[data-testid="ui/message/4000001"]').should("to.exist") + cy.get(websiteTrait).should("have.value", "b") + cy.get(emailTrait).should("have.value", email) + cy.get(websiteTrait).clear() + cy.get(websiteTrait).type("https://www.ory.sh") + + cy.get('[name="passkey_register_trigger"]').click() + + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + + it("should pass transient_payload to webhook", () => { + testRegistrationWebhook( + (hooks) => cy.setupHooks("registration", "after", "passkey", hooks), + () => { + signup(registration, app) + }, + ) + }) + + it("should be able to login with registered account", () => { + const email = gen.email() + + signup(registration, app, email) + cy.logout() + cy.visit(login) + + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + + it("should not be able to unlink last passkey", () => { + const email = gen.email() + signup(registration, app, email) + cy.visit(settings) + cy.get('[name="passkey_remove"]').should("have.attr", "disabled") + }) + + it("should be able to link password and use both methods for sign in", () => { + const email = gen.email() + const password = gen.password() + signup(registration, app, email) + cy.visit(settings) + cy.get('[name="passkey_remove"]').should("have.attr", "disabled") + cy.get('[name="password"]').type(password) + cy.get('[value="password"]').click() + cy.expectSettingsSaved() + cy.get('[name="passkey_remove"]').click() + cy.expectSettingsSaved() + cy.logout() + cy.visit(login) + + cy.get('[name="identifier"]').type(email) + cy.get('[name="password"]').type(password) + cy.get('[name="method"][value="password"]').click() + }) + + it("should be able to refresh", () => { + const email = gen.email() + signup(registration, app, email) + cy.visit(login + "?refresh=true") + cy.get('[name="identifier"][type="hidden"]').should("exist") + cy.get('[name="identifier"][type="input"]').should("not.exist") + cy.get('[name="password"]').should("not.exist") + cy.get('[value="password"]').should("not.exist") + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey", "passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + + it("should not be able to use for MFA", () => { + const email = gen.email() + signup(registration, app, email) + cy.visit(login + "?aal=aal2") + cy.get('[value="passkey"]').should("not.exist") + cy.get('[name="passkey_login_trigger"]').should("not.exist") + }) + + it("should be able to add method later and try a variety of refresh flows", () => { + const email = gen.email() + const password = gen.password() + cy.visit(registration) + + const emailTrait = `${ + app === "express" ? `[data-testid="registration-flow"]` : "" + } [name="traits.email"]` + const websiteTrait = `${ + app === "express" ? `[data-testid="registration-flow"]` : "" + } [name="traits.website"]` + + cy.get(emailTrait).type(email) + cy.get('[name="password"]').type(password) + cy.get(websiteTrait).type("https://www.ory.sh") + cy.get('[value="password"]').click() + cy.location("pathname").should("not.contain", "/registration") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password"], + }) + + cy.visit(settings) + cy.get('[name="passkey_register_trigger"]').click() + cy.expectSettingsSaved() + + cy.visit(login + "?refresh=true") + cy.get('[name="password"]').should("exist") + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + cy.location("pathname").should("not.contain", "/login") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password", "passkey", "passkey"], + }) + + cy.visit(login + "?refresh=true") + cy.get('[name="password"]').type(password) + cy.get('[value="password"]').click() + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password", "passkey", "passkey", "password"], + }) + + cy.logout() + cy.visit(login) + + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts b/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts index 390491fe8f0d..adf4505dfaff 100644 --- a/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts +++ b/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts @@ -115,7 +115,8 @@ context("Passwordless registration", () => { cy.get('[data-testid="ui/message/4000001"]').should("to.exist") cy.get(websiteTrait).should("have.value", "b") cy.get(emailTrait).should("have.value", email) - cy.get(websiteTrait).clear().type("https://www.ory.sh") + cy.get(websiteTrait).clear() + cy.get(websiteTrait).type("https://www.ory.sh") cy.clickWebAuthButton("register") cy.getSession({ expectAal: "aal1", @@ -139,6 +140,26 @@ context("Passwordless registration", () => { ) }) + // I have no idea why this does not work in an E2E test. It works just fine manually. + xit("should use webauthn credential as passkey", () => { + const email = gen.email() + + signup(registration, app, email) + cy.logout() + cy.visit(login) + + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + it("should be able to login with registered account", () => { const email = gen.email() @@ -168,7 +189,7 @@ context("Passwordless registration", () => { const email = gen.email() signup(registration, app, email) cy.visit(settings) - cy.get('[name="webauthn_remove"]').should("not.exist") + cy.get('[name="webauthn_remove"]').should("be.disabled") }) it("should be able to link password and use both methods for sign in", () => { @@ -176,7 +197,7 @@ context("Passwordless registration", () => { const password = gen.password() signup(registration, app, email) cy.visit(settings) - cy.get('[name="webauthn_remove"]').should("not.exist") + cy.get('[name="webauthn_remove"]').should("be.disabled") cy.get('[name="password"]').type(password) cy.get('[value="password"]').click() cy.expectSettingsSaved() @@ -295,7 +316,7 @@ context("Passwordless registration", () => { cy.get('[name="webauthn_login_trigger"]').should("not.exist") cy.visit(settings) - cy.get('[name="webauthn_remove"]').should("not.exist") + cy.get('[name="webauthn_remove"]').should("be.disabled") cy.get('[name="webauthn_register_displayname"]').type("key2") cy.clickWebAuthButton("register") cy.expectSettingsSaved() diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts new file mode 100644 index 000000000000..2b09fa1e6745 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts @@ -0,0 +1,385 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Session } from "@ory/kratos-client" +import { gen, MOBILE_URL } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration success with code method", () => { + ;[ + { + route: express.registration, + login: express.login, + recovery: express.recovery, + app: "express" as "express", + profile: "two-steps", + }, + { + route: react.registration, + login: react.login, + recovery: react.recovery, + app: "react" as "react", + profile: "two-steps", + }, + { + route: MOBILE_URL + "/Registration", + login: MOBILE_URL + "/Login", + recovery: MOBILE_URL + "/Recovery", + app: "mobile" as "mobile", + profile: "two-steps", + }, + ].forEach(({ route, login, recovery, profile, app }) => { + describe(`for app ${app}`, () => { + const Selectors = { + mobile: { + identifier: "[data-testid='field/identifier']", + recoveryEmail: "[data-testid='field/email']", + email: "[data-testid='traits.email']", + email2: "[data-testid='traits.email2']", + website: "[data-testid='traits.website']", + username: "[data-testid='traits.username']", + code: "[data-testid='field/code'] input", + recoveryCode: "[data-testid='code']", + submitCode: "[data-testid='field/method/code']", + resendCode: "[data-testid='field/resend/code']", + credentialSelection: + "[data-testid='field/screen/credential-selection']", + submitRecovery: "[data-testid='field/method/code']", + codeHiddenMethod: "[data-testid='field/method/code']", + }, + express: { + identifier: + "[data-testid='login-flow-code'] input[name='identifier']", + recoveryEmail: "input[name=email]", + email: + "[data-testid='node/input/traits.email'] input[name='traits.email']", + email2: + "[data-testid='node/input/traits.email2'] input[name='traits.email2']", + website: + "[data-testid='node/input/traits.website'] [name='traits.website']", + username: + "[data-testid='node/input/traits.username'] input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + credentialSelection: "[name='screen'][value='credential-selection']", + }, + react: { + identifier: "input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "input[name='traits.email']", + email2: "input[name='traits.email2']", + website: "[name='traits.website']", + username: "input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + credentialSelection: "[name='screen'][value='credential-selection']", + }, + } + + before(() => { + cy.deleteMail() + cy.useConfigProfile(profile) + if (app !== "mobile") { + cy.proxy(app) + } + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + }) + + it("should be able to resend the registration code", async () => { + const email = gen.email() + const website = "https://www.example.org/" + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["website"]).type(website) + + cy.submitProfileForm(app) + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).then((code) => + cy.wrap(code).as("code1"), + ) + + // cy.get(Selectors[app]["email"]).should("have.value", email) + cy.get(Selectors[app]["codeHiddenMethod"]).should("exist") + cy.get(Selectors[app]["resendCode"]).click() + + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code1").then((code1) => { + // previous code should not work + cy.get(Selectors[app]["code"]).clear() + cy.get(Selectors[app]["code"]).type(code1.toString()) + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/4040003"]').should( + "contain.text", + "The registration code is invalid or has already been used. Please try again.", + ) + }) + + // Navigate back and forth again + cy.get(Selectors[app]["credentialSelection"]).click() + cy.submitCodeForm(app) + + // Mobile app sends another email when we go back and forth. + if (app === "mobile") { + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.wrap(code).as("code2") + }) + } + + cy.get("@code2").then((code2) => { + cy.get(Selectors[app]["code"]).clear() + cy.get(Selectors[app]["code"]).type(code2.toString()) + cy.submitCodeForm(app) + }) + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should sign up and be logged in with session hook", () => { + const email = gen.email() + const website = "https://www.example.org/" + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["website"]).type(website) + cy.submitProfileForm(app) + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to sign up without session hook", () => { + cy.setPostCodeRegistrationHooks([]) + const email = gen.email() + const website = "https://www.example.org/" + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["website"]).type(website) + cy.submitProfileForm(app) + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + cy.visit(login) + cy.get(Selectors[app]["identifier"]).type(email) + cy.get(Selectors[app]["submitCode"]).click() + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + // Try keep this test as the last one, as it updates the identity schema. + it("should be able to use multiple identifiers to signup with and sign in to", () => { + cy.setPostCodeRegistrationHooks([ + { + hook: "session", + }, + ]) + + // Setup complex schema + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.complex.traits.schema.json", + ) + + cy.visit(route) + + cy.get(Selectors[app]["username"]).type(Math.random().toString(36)) + + const email = gen.email() + cy.get(Selectors[app]["email"]).type(email) + + const email2 = gen.email() + cy.get(Selectors[app]["email2"]).type(email2) + + cy.submitProfileForm(app) + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + // intentionally use email 1 to sign up for the account + cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).should( + (code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }, + ) + + if (app === "mobile") { + cy.visit(MOBILE_URL + "/Home") + cy.get('*[data-testid="logout"]').click() + } else { + cy.logout() + } + + // There are verification emails from the registration process in the inbox that we need to deleted + // for the assertions below to pass. + cy.deleteMail({ atLeast: 1 }) + + // Attempt to sign in with email 2 (should fail) + cy.visit(login) + cy.get(Selectors[app]["identifier"]).type(email2) + + cy.get(Selectors[app]["submitCode"]).click() + + cy.getLoginCodeFromEmail(email2, { + expectedCount: 1, + }).should((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(2) + expect( + identity.verifiable_addresses.filter((v) => v.value === email)[0] + .status, + ).to.equal("completed") + expect( + identity.verifiable_addresses.filter((v) => v.value === email2)[0] + .status, + ).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts new file mode 100644 index 000000000000..33b8bafe8735 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts @@ -0,0 +1,216 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { testRegistrationWebhook } from "../../../../helpers/webhook" + +context("Social Sign Up Successes", () => { + ;[ + { + login: react.login, + registration: react.registration, + app: "react" as "react", + profile: "two-steps", + }, + { + login: express.login, + registration: express.registration, + app: "express" as "express", + profile: "two-steps", + }, + ].forEach(({ registration, login, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(registration) + cy.setIdentitySchema( + "file://test/e2e/profiles/oidc/identity.traits.schema.json", + ) + }) + + const shouldSession = (email) => (session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.website).to.equal(website) + expect(identity.traits.email).to.equal(email) + } + + it("should be able to sign up with incomplete data and finally be signed in", () => { + const email = gen.email() + + cy.registerOidc({ + app, + email, + expectSession: false, + route: registration, + }) + + cy.get("#registration-password").should("not.exist") + cy.get(appPrefix(app) + '[name="traits.email"]').should( + "have.value", + email, + ) + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property website is missing", + ) + + cy.get('[name="traits.consent"][type="checkbox"]') + .siblings("label") + .click() + cy.get('[name="traits.newsletter"][type="checkbox"]') + .siblings("label") + .click() + cy.get('[name="traits.website"]').type("http://s") + + cy.get('[name="provider"]') + .should("have.length", 1) + .should("have.value", "hydra") + .should("contain.text", "Continue") + .click() + + cy.get("#registration-password").should("not.exist") + cy.get('[name="traits.email"]').should("have.value", email) + cy.get('[name="traits.website"]').should("have.value", "http://s") + cy.get('[data-testid="ui/message/4000003"]').should( + "contain.text", + "length must be >= 10", + ) + cy.get('[name="traits.website"]') + .should("have.value", "http://s") + .clear() + .type(website) + + cy.get('[name="traits.consent"]').should("be.checked") + cy.get('[name="traits.newsletter"]').should("be.checked") + + cy.triggerOidc(app) + + cy.location("pathname").should((loc) => { + expect(loc).to.be.oneOf(["/welcome", "/", "/sessions"]) + }) + + cy.getSession().should((session) => { + shouldSession(email)(session) + expect(session.identity.traits.consent).to.equal(true) + }) + }) + + it("should pass transient_payload to webhook", () => { + testRegistrationWebhook( + (hooks) => cy.setupHooks("registration", "after", "oidc", hooks), + () => { + const email = gen.email() + cy.registerOidc({ + app, + email, + website, + route: registration, + }) + cy.getSession().should(shouldSession(email)) + }, + ) + }) + + it("should be able to sign up with complete data", () => { + const email = gen.email() + + cy.registerOidc({ app, email, website, route: registration }) + cy.getSession().should(shouldSession(email)) + }) + + it("should be able to convert a sign up flow to a sign in flow", () => { + const email = gen.email() + + cy.registerOidc({ app, email, website, route: registration }) + cy.logout() + cy.noSession() + cy.visit(registration) + cy.triggerOidc(app) + + cy.location("pathname").should((path) => { + expect(path).to.oneOf(["/", "/welcome", "/sessions"]) + }) + + cy.getSession().should(shouldSession(email)) + }) + + it("should be able to convert a sign in flow to a sign up flow", () => { + cy.setIdentitySchema( + "file://test/e2e/profiles/oidc/identity-required.traits.schema.json", + ) + + const email = gen.email() + cy.visit(login) + cy.triggerOidc(app) + + cy.get("#username").clear().type(email) + cy.get("#remember").click() + cy.get("#accept").click() + cy.get('[name="scope"]').each(($el) => cy.wrap($el).click()) + cy.get("#remember").click() + cy.get("#accept").click() + + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property website is missing", + ) + cy.get('[name="traits.website"]').type("http://s") + + cy.triggerOidc(app) + + cy.get('[data-testid="ui/message/4000003"]').should( + "contain.text", + "length must be >= 10", + ) + cy.get('[name="traits.requirednested"]').should("not.exist") + cy.get('[name="traits.requirednested.a"]').siblings("label").click() + cy.get('[name="traits.consent"]').siblings("label").click() + cy.get('[name="traits.website"]') + .should("have.value", "http://s") + .clear() + .type(website) + cy.triggerOidc(app) + + cy.location("pathname").should("not.contain", "/registration") + + cy.getSession().should(shouldSession(email)) + }) + + it("should be able to sign up with redirects", () => { + const email = gen.email() + cy.registerOidc({ + app, + email, + website, + route: registration + "?return_to=https://www.ory.sh/", + }) + cy.location("href").should("eq", "https://www.ory.sh/") + cy.logout() + }) + + it("should be able to register with upstream parameters", () => { + const email = gen.email() + cy.intercept("GET", "**/oauth2/auth*").as("getHydraRegistration") + + cy.visit(registration + "?return_to=https://www.example.org/") + + cy.addInputElement("form", "upstream_parameters.login_hint", email) + + cy.triggerOidc(app) + + // once a request to getHydraRegistration responds, 'cy.wait' will resolve + cy.wait("@getHydraRegistration") + .its("request.url") + .should("include", "login_hint=" + encodeURIComponent(email)) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts new file mode 100644 index 000000000000..975306f8c857 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration success with two-step signup", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "two-steps", + }, + { + route: react.registration, + app: "react" as "react", + profile: "two-steps", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + cy.enableVerification() + if (app === "express") { + cy.enableVerificationUIAfterRegistration("password") + } + }) + + it("should sign up and be logged in", () => { + const email = gen.email() + const password = gen.password() + const website = "https://www.example.org/" + + // Fill out step one forms + cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type("someone@foo") + cy.get('input[name="traits.website"]').type(website) + cy.get('[name="method"][value="profile"]').click() + + // navigate back, fill traits again + cy.get('[name="method"][value="profile:back"]').click() + cy.get('input[name="traits.email"]').type( + "{selectall}{backspace}" + email, + ) + cy.get('[name="method"][value="profile"]').click() + + // Fill out step two forms + cy.get('input[name="password"]').type(password) + cy.get('[name="method"][value="password"]').click() + + if (app === "express") { + cy.get('a[href*="sessions"]').click() + } + cy.get("pre").should("contain.text", email) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.schema_id).to.equal("default") + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) + expect(identity.traits.website).to.equal(website) + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should handle form errors", () => { + const email = gen.email() + const password = gen.password() + const websiteTooShort = "a://b" + const website = "https://www.example.com" + + // Fill out step one forms + cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="traits.website"]').type(websiteTooShort) + cy.get('[name="method"][value="profile"]').click() + + // Assert form errors + cy.get('[data-testid="ui/message/4000003"]').should("to.exist") + + // Enter correct value + cy.get('input[name="traits.website"]').type( + "{selectall}{backspace}" + website, + ) + cy.get('[name="method"][value="profile"]').click() + + // Fill out step two forms + cy.get('input[name="password"]').type(password) + cy.get('[name="method"][value="password"]').click() + + if (app === "express") { + cy.get('a[href*="sessions"]').click() + } + cy.get("pre").should("contain.text", email) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.schema_id).to.equal("default") + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) + expect(identity.traits.website).to.equal(website) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 7ef6260f4b78..cfaef5ac9185 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -1348,9 +1348,14 @@ Cypress.Commands.add("submitPasswordForm", () => { cy.get('[name="method"][value="password"]:disabled').should("not.exist") }) -Cypress.Commands.add("submitProfileForm", () => { - cy.get('[name="method"][value="profile"]').click() - cy.get('[name="method"][value="profile"]:disabled').should("not.exist") +Cypress.Commands.add("submitProfileForm", (app?: string) => { + if (app === "mobile") { + cy.get('[data-testid="field/method/profile"]').click() + cy.get('[data-testid="field/method/profile"]:disabled').should("not.exist") + } else { + cy.get('[name="method"][value="profile"]').click() + cy.get('[name="method"][value="profile"]:disabled').should("not.exist") + } }) Cypress.Commands.add("submitCodeForm", (app) => { diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index 3fef679ec098..9cfeb083f12a 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -38,7 +38,12 @@ declare global { getSession(opts?: { expectAal?: "aal2" | "aal1" expectMethods?: Array< - "password" | "webauthn" | "lookup_secret" | "totp" | "code" + | "password" + | "webauthn" + | "lookup_secret" + | "totp" + | "code" + | "passkey" > token?: string }): Chainable @@ -186,7 +191,7 @@ declare global { | "verification" | "settings", phase: "before" | "after", - kind: "password" | "webauthn" | "oidc" | "code", + kind: "password" | "webauthn" | "oidc" | "code" | "passkey", hooks: Array<{ hook: string; config?: any }>, ): Chainable @@ -359,7 +364,7 @@ declare global { /** * Submits a profile form by clicking the button with method=profile */ - submitProfileForm(): Chainable + submitProfileForm(app?: "mobile" | "express" | "react"): Chainable /** * Submits a code form by clicking the button with method=code diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 58f050aa83e9..0db7cd92b1ca 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: code: diff --git a/test/e2e/profiles/email/.kratos.yml b/test/e2e/profiles/email/.kratos.yml index 2176f282084a..b1d62a3e25c4 100644 --- a/test/e2e/profiles/email/.kratos.yml +++ b/test/e2e/profiles/email/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/profiles/mfa/.kratos.yml b/test/e2e/profiles/mfa/.kratos.yml index 1ce3f4ec8724..99becd59a868 100644 --- a/test/e2e/profiles/mfa/.kratos.yml +++ b/test/e2e/profiles/mfa/.kratos.yml @@ -10,6 +10,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/profiles/mobile/.kratos.yml b/test/e2e/profiles/mobile/.kratos.yml index 329c56b6f051..c0a46e57c197 100644 --- a/test/e2e/profiles/mobile/.kratos.yml +++ b/test/e2e/profiles/mobile/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true after: password: hooks: diff --git a/test/e2e/profiles/network/.kratos.yml b/test/e2e/profiles/network/.kratos.yml index dd4e70671995..c3c8b3daedd7 100644 --- a/test/e2e/profiles/network/.kratos.yml +++ b/test/e2e/profiles/network/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: hooks: diff --git a/test/e2e/profiles/oidc-provider/.kratos.yml b/test/e2e/profiles/oidc-provider/.kratos.yml index 4850e71a380b..09b2c9978700 100644 --- a/test/e2e/profiles/oidc-provider/.kratos.yml +++ b/test/e2e/profiles/oidc-provider/.kratos.yml @@ -31,6 +31,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: oidc: diff --git a/test/e2e/profiles/oidc/.kratos.yml b/test/e2e/profiles/oidc/.kratos.yml index d1d6b5f696b5..b0a327bb5096 100644 --- a/test/e2e/profiles/oidc/.kratos.yml +++ b/test/e2e/profiles/oidc/.kratos.yml @@ -40,6 +40,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/profiles/passkey/.kratos.yml b/test/e2e/profiles/passkey/.kratos.yml new file mode 100644 index 000000000000..cbe13e07ef1e --- /dev/null +++ b/test/e2e/profiles/passkey/.kratos.yml @@ -0,0 +1,55 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + required_aal: aal1 + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + enable_legacy_one_step: true + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + passkey: + hooks: + - hook: session + + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + recovery: + ui_url: http://localhost:4455/recovery + + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + passkey: + enabled: true + config: + rp: + id: localhost + origins: + - http://localhost:4455 + display_name: Ory + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/passkey/identity.traits.schema.json + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/passkey/identity.traits.schema.json b/test/e2e/profiles/passkey/identity.traits.schema.json new file mode 100644 index 000000000000..8dbece14adca --- /dev/null +++ b/test/e2e/profiles/passkey/identity.traits.schema.json @@ -0,0 +1,40 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/passwordless/.kratos.yml b/test/e2e/profiles/passwordless/.kratos.yml index 7a7e7c5c6267..b3582a61216c 100644 --- a/test/e2e/profiles/passwordless/.kratos.yml +++ b/test/e2e/profiles/passwordless/.kratos.yml @@ -10,6 +10,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: @@ -18,6 +19,9 @@ selfservice: webauthn: hooks: - hook: session + passkey: + hooks: + - hook: session login: ui_url: http://localhost:4455/login @@ -43,6 +47,16 @@ selfservice: id: localhost origin: http://localhost:4455 display_name: Ory + passkey: + enabled: true + config: + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origins: + - http://localhost:4455 identity: schemas: diff --git a/test/e2e/profiles/passwordless/identity.traits.schema.json b/test/e2e/profiles/passwordless/identity.traits.schema.json index 87db142ee8d2..9218e7126434 100644 --- a/test/e2e/profiles/passwordless/identity.traits.schema.json +++ b/test/e2e/profiles/passwordless/identity.traits.schema.json @@ -19,6 +19,9 @@ }, "webauthn": { "identifier": true + }, + "passkey": { + "display_name": true } } } @@ -30,10 +33,7 @@ "minLength": 10 } }, - "required": [ - "email", - "website" - ], + "required": ["email", "website"], "additionalProperties": false } } diff --git a/test/e2e/profiles/spa/.kratos.yml b/test/e2e/profiles/spa/.kratos.yml index 56590081105d..6d5eb44a67de 100644 --- a/test/e2e/profiles/spa/.kratos.yml +++ b/test/e2e/profiles/spa/.kratos.yml @@ -10,6 +10,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/profiles/two-steps/.kratos.yml b/test/e2e/profiles/two-steps/.kratos.yml new file mode 100644 index 000000000000..d23dd0bce07c --- /dev/null +++ b/test/e2e/profiles/two-steps/.kratos.yml @@ -0,0 +1,110 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + required_aal: aal1 + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + enable_legacy_one_step: false + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + webauthn: + hooks: + - hook: session + code: + hooks: + - hook: session + oidc: + hooks: + - hook: session + + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + recovery: + enabled: true + use: code + ui_url: http://localhost:4455/recovery + + methods: + password: + enabled: true + + webauthn: + enabled: true + config: + passwordless: true + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origin: http://localhost:4455 + + totp: + config: + issuer: Kratos + enabled: true + + lookup_secret: + enabled: true + + link: + enabled: true + + code: + enabled: true + passwordless_enabled: true + passwordless_login_fallback_enabled: false + config: + lifespan: 1h + + oidc: + enabled: true + config: + providers: + - id: hydra + label: Ory + provider: generic + client_id: ${OIDC_HYDRA_CLIENT_ID} + client_secret: ${OIDC_HYDRA_CLIENT_SECRET} + issuer_url: http://localhost:4444/ + scope: + - offline + mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + - id: google + provider: generic + client_id: ${OIDC_GOOGLE_CLIENT_ID} + client_secret: ${OIDC_GOOGLE_CLIENT_SECRET} + issuer_url: http://localhost:4444/ + scope: + - offline + mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + - id: github + provider: generic + client_id: ${OIDC_GITHUB_CLIENT_ID} + client_secret: ${OIDC_GITHUB_CLIENT_SECRET} + issuer_url: http://localhost:4444/ + scope: + - offline + mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/two-steps/identity.traits.schema.json + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/two-steps/identity.traits.schema.json b/test/e2e/profiles/two-steps/identity.traits.schema.json new file mode 100644 index 000000000000..51b2a1cdb91c --- /dev/null +++ b/test/e2e/profiles/two-steps/identity.traits.schema.json @@ -0,0 +1,41 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + }, + "webauthn": { + "identifier": true + } + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + } + }, + "required": ["email", "website"], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/webhooks/.kratos.yml b/test/e2e/profiles/webhooks/.kratos.yml index f29486df20e3..00eb10537adb 100644 --- a/test/e2e/profiles/webhooks/.kratos.yml +++ b/test/e2e/profiles/webhooks/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 863a70bb910b..c0af9664faa2 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -253,7 +253,7 @@ run() { nc -zv localhost 4433 && exit 1 ls -la . - for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do + for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless passkey webhooks oidc-provider oidc-provider-mfa two-steps; do yq ea '. as $item ireduce ({}; . * $item )' test/e2e/profiles/kratos.base.yml "test/e2e/profiles/${profile}/.kratos.yml" > test/e2e/kratos.${profile}.yml cat "test/e2e/kratos.${profile}.yml" | envsubst | sponge "test/e2e/kratos.${profile}.yml" done diff --git a/text/id.go b/text/id.go index 562a179740ef..a466caec0f8c 100644 --- a/text/id.go +++ b/text/id.go @@ -30,6 +30,7 @@ const ( InfoSelfServiceLoginWithAndLink // 1010018 InfoSelfServiceLoginCodeMFA // 1010019 InfoSelfServiceLoginCodeMFAHint // 1010020 + InfoSelfServiceLoginPasskey // 1010021 ) const ( @@ -48,6 +49,9 @@ const ( InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 InfoSelfServiceRegistrationEmailWithCodeSent // 1040005 InfoSelfServiceRegistrationRegisterCode // 1040006 + InfoSelfServiceRegistrationRegisterPasskey // 1040007 + InfoSelfServiceRegistrationBack // 1040008 + InfoSelfServiceRegistrationChooseCredentials // 1040009 ) const ( @@ -70,6 +74,8 @@ const ( InfoSelfServiceSettingsDisableLookup InfoSelfServiceSettingsTOTPSecretLabel InfoSelfServiceSettingsRemoveWebAuthn + InfoSelfServiceSettingsRegisterPasskey + InfoSelfServiceSettingsRemovePasskey ) const ( diff --git a/text/message_login.go b/text/message_login.go index c94db4538704..ec627458a028 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -187,6 +187,14 @@ func NewInfoSelfServiceLoginWebAuthn() *Message { } } +func NewInfoSelfServiceLoginPasskey() *Message { + return &Message{ + ID: InfoSelfServiceLoginPasskey, + Text: "Sign in with passkey", + Type: Info, + } +} + func NewInfoSelfServiceContinueLoginWebAuthn() *Message { return &Message{ ID: InfoSelfServiceLoginContinueWebAuthn, diff --git a/text/message_registration.go b/text/message_registration.go index 2dcf807a4766..7779143a1cc0 100644 --- a/text/message_registration.go +++ b/text/message_registration.go @@ -35,6 +35,22 @@ func NewInfoRegistrationContinue() *Message { } } +func NewInfoRegistrationBack() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationBack, + Text: "Back", + Type: Info, + } +} + +func NewInfoSelfServiceChooseCredentials() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationChooseCredentials, + Text: "Please choose a credential to authenticate yourself with.", + Type: Info, + } +} + func NewErrorValidationRegistrationFlowExpired(expiredAt time.Time) *Message { return &Message{ ID: ErrorValidationRegistrationFlowExpired, @@ -55,6 +71,14 @@ func NewInfoSelfServiceRegistrationRegisterWebAuthn() *Message { } } +func NewInfoSelfServiceRegistrationRegisterPasskey() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterPasskey, + Text: "Sign up with passkey", + Type: Info, + } +} + func NewRegistrationEmailWithCodeSent() *Message { return &Message{ ID: InfoSelfServiceRegistrationEmailWithCodeSent, diff --git a/text/message_settings.go b/text/message_settings.go index 9cfeb370c33c..97f0ff0266c6 100644 --- a/text/message_settings.go +++ b/text/message_settings.go @@ -166,6 +166,14 @@ func NewInfoSelfServiceSettingsRegisterWebAuthn() *Message { } } +func NewInfoSelfServiceSettingsRegisterPasskey() *Message { + return &Message{ + ID: InfoSelfServiceSettingsRegisterPasskey, + Text: "Add passkey", + Type: Info, + } +} + func NewInfoSelfServiceRegisterWebAuthnDisplayName() *Message { return &Message{ ID: InfoSelfServiceSettingsRegisterWebAuthnDisplayName, @@ -186,3 +194,16 @@ func NewInfoSelfServiceRemoveWebAuthn(name string, createdAt time.Time) *Message }), } } + +func NewInfoSelfServiceRemovePasskey(name string, createdAt time.Time) *Message { + return &Message{ + ID: InfoSelfServiceSettingsRemovePasskey, + Text: fmt.Sprintf("Remove passkey \"%s\"", name), + Type: Info, + Context: context(map[string]any{ + "display_name": name, + "added_at": createdAt, + "added_at_unix": createdAt.Unix(), + }), + } +} diff --git a/ui/node/attributes.go b/ui/node/attributes.go index 346e523801c9..daf56c9fc675 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -93,6 +93,10 @@ type InputAttributes struct { // used for WebAuthn. OnClick string `json:"onclick,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily + // used for WebAuthn. + OnLoad string `json:"onload,omitempty"` + // NodeType represents this node's types. It is a mirror of `node.type` and // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input". // diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go index 78e8a84d2897..b63ac9365a51 100644 --- a/ui/node/attributes_input.go +++ b/ui/node/attributes_input.go @@ -70,12 +70,6 @@ func applyImageAttributes(opts ImageAttributesModifiers, attributes *ImageAttrib type ScriptAttributesModifier func(attributes *ScriptAttributes) type ScriptAttributesModifiers []ScriptAttributesModifier -func WithScriptAttributes(f func(a *ScriptAttributes)) func(a *ScriptAttributes) { - return func(a *ScriptAttributes) { - f(a) - } -} - func applyScriptAttributes(opts ScriptAttributesModifiers, attributes *ScriptAttributes) *ScriptAttributes { for _, f := range opts { f(attributes) diff --git a/ui/node/identifiers.go b/ui/node/identifiers.go index 17ed2dd88c27..16cf2e1acc4e 100644 --- a/ui/node/identifiers.go +++ b/ui/node/identifiers.go @@ -19,6 +19,10 @@ const ( LookupCodeEnter = "lookup_secret" ) +const ( + ProfileChooseCredentials = "profile_choose_credentials" +) + const ( WebAuthnRegisterTrigger = "webauthn_register_trigger" WebAuthnRegister = "webauthn_register" @@ -28,3 +32,14 @@ const ( WebAuthnRemove = "webauthn_remove" WebAuthnScript = "webauthn_script" ) + +const ( + PasskeyRegisterTrigger = "passkey_register_trigger" + PasskeyRegister = "passkey_register" + PasskeySettingsRegister = "passkey_settings_register" + PasskeyCreateData = "passkey_create_data" + PasskeyLogin = "passkey_login" + PasskeyChallenge = "passkey_challenge" + PasskeyLoginTrigger = "passkey_login_trigger" //#nosec G101 -- Not a credential + PasskeyRemove = "passkey_remove" +) diff --git a/ui/node/node.go b/ui/node/node.go index c1c2aa64f1c0..c4e9e05519f8 100644 --- a/ui/node/node.go +++ b/ui/node/node.go @@ -48,6 +48,7 @@ const ( TOTPGroup UiNodeGroup = "totp" LookupGroup UiNodeGroup = "lookup_secret" WebAuthnGroup UiNodeGroup = "webauthn" + PasskeyGroup UiNodeGroup = "passkey" ) func (g UiNodeGroup) String() string { diff --git a/x/webauthnx/aaguid/aaguid.go b/x/webauthnx/aaguid/aaguid.go new file mode 100644 index 000000000000..376303848bc9 --- /dev/null +++ b/x/webauthnx/aaguid/aaguid.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:generate curl https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/main/aaguid.json --output passkey-aaguids.json + +package aaguid + +import ( + _ "embed" + "encoding/json" + + "github.com/gofrs/uuid" + "golang.org/x/exp/maps" +) + +var ( + //go:embed aaguids.json + rawAAGUIDs []byte + //go:embed passkey-aaguids.json + rawPasskeyAAGUIDs []byte + aaguids map[string]AAGUID +) + +type AAGUID struct { + Name string `json:"name"` + IconDark string `json:"icon_dark"` + IconLight string `json:"icon_light"` +} + +func init() { + err := json.Unmarshal(rawAAGUIDs, &aaguids) + if err != nil { + panic(err) + } + + var passkeyAAGUIDs map[string]AAGUID + err = json.Unmarshal(rawPasskeyAAGUIDs, &passkeyAAGUIDs) + if err != nil { + panic(err) + } + + maps.Copy(aaguids, passkeyAAGUIDs) +} + +func Lookup(id []byte) *AAGUID { + uid, err := uuid.FromBytes(id) + if err != nil { + return nil + } + + if aaguid, ok := aaguids[uid.String()]; ok { + return &aaguid + } + return nil +} diff --git a/x/webauthnx/aaguid/aaguid_test.go b/x/webauthnx/aaguid/aaguid_test.go new file mode 100644 index 000000000000..8f7d83a8b315 --- /dev/null +++ b/x/webauthnx/aaguid/aaguid_test.go @@ -0,0 +1,51 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package aaguid + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" +) + +func TestLookup(t *testing.T) { + goodUUIDBytes := uuid.Must(uuid.FromString("adce0002-35bc-c60a-648b-0b25f1f05503")).Bytes() + badUUIDBytes := uuid.Must(uuid.NewV4()).Bytes() + + tests := []struct { + name string + id []byte + want string + }{ + { + name: "GoodUUID", + id: goodUUIDBytes, + want: "Chrome on Mac", + }, + { + name: "BadUUID", + id: badUUIDBytes, + }, + { + name: "NilUUID", + id: nil, + }, + { + name: "EmptyUUID", + id: []byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := Lookup(tt.id) + if tt.want == "" { + assert.Nil(t, res) + } else { + assert.Equal(t, tt.want, res.Name) + } + }) + } +} diff --git a/x/webauthnx/aaguid/aaguids.json b/x/webauthnx/aaguid/aaguids.json new file mode 100644 index 000000000000..85fa3dcd20b0 --- /dev/null +++ b/x/webauthnx/aaguid/aaguids.json @@ -0,0 +1,603 @@ +{ + "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": { + "name": "Google Password Manager", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=" + }, + "adce0002-35bc-c60a-648b-0b25f1f05503": { + "name": "Chrome on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==" + }, + "08987058-cadc-4b81-b6e1-30de50dcbe96": { + "name": "Windows Hello Hardware Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" + }, + "9ddd1817-af5a-4672-a2b9-3e3dd95000a9": { + "name": "Windows Hello VBS Hardware Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" + }, + "6028b017-b1d4-4c02-b4b3-afcdafc96bb2": { + "name": "Windows Hello Software Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" + }, + "dd4ec289-e01d-41c9-bb89-70fa845d4bf2": { + "name": "Apple iCloud Keychain (Managed)" + }, + "531126d6-e717-415c-9320-3d9aa6981239": { + "name": "Dashlane", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDMyMS4xMDRDMzUyLjczNiAzMTguMDg0IDM0OS43MTcgMzE1LjA2NCAzNDUuNDc3IDMxMy44NTRMMjk0LjY3NCAyOTUuMTI2QzI4Ni4xNTcgMjkxLjQ5NiAyNzYuNTI2IDI5NS43MjYgMjc2LjUyNiAzMDEuNzg1VjQzNy42NzRDMjc2LjUyNiA0NDAuNzA0IDI3OS41NDYgNDQ0LjMzMyAyODIuNTY2IDQ0NS41NDNMMzM0LjU4OSA0NjQuMjcxQzM0Mi40NTggNDY3LjI5MSAzNTIuNzM2IDQ2My4wNjEgMzUyLjczNiA0NTYuNDAzVjMyMS4xMDRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjU3LjQ3NCAzNS4zMjExQzI1Ny40NzQgMzIuMzAxMyAyNTQuNDU0IDI5LjI4MTUgMjUwLjIxNSAyOC4wNzE3TDE5OS40MTEgOS4zNDM0M0MxOTAuODk1IDUuNzEzOTkgMTgxLjI2NCA5Ljk0MzU4IDE4MS4yNjQgMTYuMDAyMlYxNTEuODkyQzE4MS4yNjQgMTU0LjkyMSAxODQuMjgzIDE1OC41NTEgMTg3LjMwMyAxNTkuNzZMMjM5LjMyNiAxNzguNDg5QzI0Ny4xOTUgMTgxLjUwOSAyNTcuNDc0IDE3Ny4yNzkgMjU3LjQ3NCAxNzAuNjJWMzUuMzIxMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDQ4IDE2OC42ODdDNDQ4IDE2NS42NjcgNDQ0Ljk4IDE2Mi42NDcgNDQwLjc0MSAxNjEuNDM3TDM4OS45MzcgMTQyLjcwOUMzODEuNDIxIDEzOS4wNzkgMzcxLjc5IDE0My4zMDkgMzcxLjc5IDE0OS4zNjhWMzYxLjQ2NkMzNzEuNzkgMzY0LjQ5NSAzNzQuODEgMzY4LjEyNSAzNzcuODI5IDM2OS4zMzVMNDI5Ljg1MiAzODguMDYzQzQzNy43MjEgMzkxLjA4MyA0NDggMzg2Ljg1MyA0NDggMzgwLjE5NFYxNjguNjg3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE2Mi4yMSAzNS4zMzA2QzE2Mi4yMSAzMi4zMTA4IDE1OS4xOSAyOS4yODE1IDE1NC45NTEgMjguMDcxN0wxMDQuMTQ4IDkuMzQzNDNDOTUuNjc4NyA1LjcxMzk5IDg2IDkuOTQzNTggODYgMTYuMDAyMlY0NzUuNzg5Qzg2IDQ3OC44MDggODkuMDE5OCA0ODIuNDM4IDkyLjA0OTIgNDgzLjY0OEwxNDQuMDYzIDUwMi4zNzZDMTUxLjkzMSA1MDUuMzk2IDE2Mi4yMSA1MDEuMTY2IDE2Mi4yMSA0OTQuNTA3VjM1LjMzMDZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTM1Mi43MzYgMzIxLjEwM0MzNTIuNzM2IDMxOC4wODQgMzQ5LjcxNyAzMTUuMDY0IDM0NS40NzcgMzEzLjg1NEwyOTQuNjc0IDI5NS4xMjZDMjg2LjE1NyAyOTEuNDk2IDI3Ni41MjYgMjk1LjcyNiAyNzYuNTI2IDMwMS43ODVWNDM3LjY3NEMyNzYuNTI2IDQ0MC43MDQgMjc5LjU0NiA0NDQuMzMzIDI4Mi41NjYgNDQ1LjU0M0wzMzQuNTg5IDQ2NC4yNzFDMzQyLjQ1OCA0NjcuMjkxIDM1Mi43MzYgNDYzLjA2MSAzNTIuNzM2IDQ1Ni40MDNWMzIxLjEwM1oiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTI1Ny40NzQgMzUuMzIxMUMyNTcuNDc0IDMyLjMwMTMgMjU0LjQ1NCAyOS4yODE1IDI1MC4yMTUgMjguMDcxN0wxOTkuNDExIDkuMzQzNDNDMTkwLjg5NSA1LjcxMzk5IDE4MS4yNjQgOS45NDM1OCAxODEuMjY0IDE2LjAwMjJWMTUxLjg5MkMxODEuMjY0IDE1NC45MjEgMTg0LjI4MyAxNTguNTUxIDE4Ny4zMDMgMTU5Ljc2TDIzOS4zMjYgMTc4LjQ4OUMyNDcuMTk1IDE4MS41MDggMjU3LjQ3NCAxNzcuMjc5IDI1Ny40NzQgMTcwLjYyVjM1LjMyMTFaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik00NDggMTY4LjY4N0M0NDggMTY1LjY2NyA0NDQuOTggMTYyLjY0NyA0NDAuNzQxIDE2MS40MzdMMzg5LjkzNyAxNDIuNzA5QzM4MS40MjEgMTM5LjA3OSAzNzEuNzkgMTQzLjMwOSAzNzEuNzkgMTQ5LjM2OFYzNjEuNDY2QzM3MS43OSAzNjQuNDk1IDM3NC44MSAzNjguMTI1IDM3Ny44MjkgMzY5LjMzNUw0MjkuODUyIDM4OC4wNjNDNDM3LjcyMSAzOTEuMDgzIDQ0OCAzODYuODUzIDQ0OCAzODAuMTk0VjE2OC42ODdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0xNjIuMjEgMzUuMzMwNkMxNjIuMjEgMzIuMzEwOCAxNTkuMTkgMjkuMjgxNSAxNTQuOTUxIDI4LjA3MTdMMTA0LjE0OCA5LjM0MzQzQzk1LjY3ODcgNS43MTM5OSA4NiA5Ljk0MzU4IDg2IDE2LjAwMjJWNDc1Ljc4OUM4NiA0NzguODA4IDg5LjAxOTggNDgyLjQzOCA5Mi4wNDkyIDQ4My42NDhMMTQ0LjA2MyA1MDIuMzc2QzE1MS45MzEgNTA1LjM5NiAxNjIuMjEgNTAxLjE2NiAxNjIuMjEgNDk0LjUwN1YzNS4zMzA2WiIgZmlsbD0iIzA5MzYzRiIvPgo8L3N2Zz4K" + }, + "bada5566-a7aa-401f-bd96-45619a55120d": { + "name": "1Password", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjI1NyAxMjAuNDE3QzIzOS4yNTcgNTQuNDE5MyAxODUuNzU1IDAuOTE2NTA0IDExOS43NTcgMC45MTY1MDRDNTMuNzYwMSAwLjkxNjUwNCAwLjI1NzMyNCA1NC40MTkzIDAuMjU3MzI0IDEyMC40MTdDMC4yNTczMjQgMTg2LjQxNyA1My43NjAxIDIzOS45MTcgMTE5Ljc1NyAyMzkuOTE3QzE4NS43NTUgMjM5LjkxNyAyMzkuMjU3IDE4Ni40MTcgMjM5LjI1NyAxMjAuNDE3Wk05OC4wMDY5IDU0LjAyNzZDOTcuMDY3NCA1NS44NzE0IDk3LjA2NzQgNTguMjg1MSA5Ny4wNjc0IDYzLjExMjZWOTAuNDcyOUM5Ny4wNjc0IDkxLjY3ODggOTcuMDY3NCA5Mi4yODE3IDk3LjIxOTYgOTIuODM5MkM5Ny4zNTQ1IDkzLjMzMzEgOTcuNTc2MyA5My43OTkgOTcuODc0NiA5NC4yMTVDOTguMjExMyA5NC42ODQ3IDk4LjY3OTIgOTUuMDY0OCA5OS42MTUyIDk1LjgyNTFMMTA2LjUzNiAxMDEuNDQ3QzEwNy42NjQgMTAyLjM2NCAxMDguMjI4IDEwMi44MjIgMTA4LjQzMyAxMDMuMzc0QzEwOC42MTMgMTAzLjg1NyAxMDguNjEzIDEwNC4zOSAxMDguNDMzIDEwNC44NzNDMTA4LjIyOCAxMDUuNDI1IDEwNy42NjQgMTA1Ljg4MyAxMDYuNTM2IDEwNi44TDk5LjYxNTIgMTEyLjQyMkM5OC42NzkzIDExMy4xODIgOTguMjExMyAxMTMuNTYyIDk3Ljg3NDYgMTE0LjAzMkM5Ny41NzYzIDExNC40NDggOTcuMzU0NSAxMTQuOTE0IDk3LjIxOTYgMTE1LjQwOEM5Ny4wNjc0IDExNS45NjUgOTcuMDY3NCAxMTYuNTY4IDk3LjA2NzQgMTE3Ljc3NFYxNzcuNzE5Qzk3LjA2NzQgMTgyLjU0NyA5Ny4wNjc0IDE4NC45NjEgOTguMDA2OSAxODYuODA1Qzk4LjgzMzMgMTg4LjQyNiAxMDAuMTUyIDE4OS43NDUgMTAxLjc3NCAxOTAuNTcxQzEwMy42MTggMTkxLjUxMSAxMDYuMDMxIDE5MS41MTEgMTEwLjg1OSAxOTEuNTExSDEyOC42NTZDMTMzLjQ4MyAxOTEuNTExIDEzNS44OTcgMTkxLjUxMSAxMzcuNzQxIDE5MC41NzFDMTM5LjM2MyAxODkuNzQ1IDE0MC42ODEgMTg4LjQyNiAxNDEuNTA4IDE4Ni44MDVDMTQyLjQ0NyAxODQuOTYxIDE0Mi40NDcgMTgyLjU0NyAxNDIuNDQ3IDE3Ny43MTlWMTUwLjM1OUMxNDIuNDQ3IDE0OS4xNTMgMTQyLjQ0NyAxNDguNTUgMTQyLjI5NSAxNDcuOTkzQzE0Mi4xNiAxNDcuNDk5IDE0MS45MzggMTQ3LjAzMyAxNDEuNjQgMTQ2LjYxN0MxNDEuMzAzIDE0Ni4xNDcgMTQwLjgzNSAxNDUuNzY3IDEzOS44OTkgMTQ1LjAwN0wxMzIuOTc4IDEzOS4zODVDMTMxLjg1IDEzOC40NjggMTMxLjI4NiAxMzguMDEgMTMxLjA4MiAxMzcuNDU5QzEzMC45MDIgMTM2Ljk3NSAxMzAuOTAyIDEzNi40NDMgMTMxLjA4MiAxMzUuOTU5QzEzMS4yODYgMTM1LjQwNyAxMzEuODUgMTM0Ljk0OSAxMzIuOTc4IDEzNC4wMzNMMTM5Ljg5OSAxMjguNDFDMTQwLjgzNSAxMjcuNjUgMTQxLjMwMyAxMjcuMjcgMTQxLjY0IDEyNi44QzE0MS45MzggMTI2LjM4NCAxNDIuMTYgMTI1LjkxOCAxNDIuMjk1IDEyNS40MjRDMTQyLjQ0NyAxMjQuODY3IDE0Mi40NDcgMTI0LjI2NCAxNDIuNDQ3IDEyMy4wNThWNjMuMTEyNkMxNDIuNDQ3IDU4LjI4NTEgMTQyLjQ0NyA1NS44NzE0IDE0MS41MDggNTQuMDI3NkMxNDAuNjgxIDUyLjQwNTcgMTM5LjM2MyA1MS4wODcgMTM3Ljc0MSA1MC4yNjA2QzEzNS44OTcgNDkuMzIxMSAxMzMuNDgzIDQ5LjMyMTEgMTI4LjY1NiA0OS4zMjExSDExMC44NTlDMTA2LjAzMSA0OS4zMjExIDEwMy42MTggNDkuMzIxMSAxMDEuNzc0IDUwLjI2MDZDMTAwLjE1MiA1MS4wODcgOTguODMzMyA1Mi40MDU3IDk4LjAwNjkgNTQuMDI3NloiIGZpbGw9IiNGRkZFRkIiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjExNiAxMjAuNDE3QzIzOS4xMTYgNTQuNDE5MyAxODUuNjEzIDAuOTE2NTA0IDExOS42MTYgMC45MTY1MDRDNTMuNjE5IDAuOTE2NTA0IDAuMTE2MjExIDU0LjQxOTMgMC4xMTYyMTEgMTIwLjQxN0MwLjExNjIxMSAxODYuNDE3IDUzLjYxOSAyMzkuOTE3IDExOS42MTYgMjM5LjkxN0MxODUuNjEzIDIzOS45MTcgMjM5LjExNiAxODYuNDE3IDIzOS4xMTYgMTIwLjQxN1pNOTcuODY1OCA1NC4wMjc2Qzk2LjkyNjMgNTUuODcxNCA5Ni45MjYzIDU4LjI4NTEgOTYuOTI2MyA2My4xMTI2VjkwLjQ3MjlDOTYuOTI2MyA5MS42Nzg4IDk2LjkyNjMgOTIuMjgxNyA5Ny4wNzg1IDkyLjgzOTJDOTcuMjEzNCA5My4zMzMxIDk3LjQzNTIgOTMuNzk5IDk3LjczMzUgOTQuMjE1Qzk4LjA3MDIgOTQuNjg0NyA5OC41MzgxIDk1LjA2NDggOTkuNDc0MSA5NS44MjUxTDEwNi4zOTUgMTAxLjQ0N0MxMDcuNTIzIDEwMi4zNjQgMTA4LjA4NyAxMDIuODIyIDEwOC4yOTIgMTAzLjM3NEMxMDguNDcxIDEwMy44NTcgMTA4LjQ3MSAxMDQuMzkgMTA4LjI5MiAxMDQuODczQzEwOC4wODcgMTA1LjQyNSAxMDcuNTIzIDEwNS44ODMgMTA2LjM5NSAxMDYuOEw5OS40NzQxIDExMi40MjJDOTguNTM4MiAxMTMuMTgyIDk4LjA3MDIgMTEzLjU2MiA5Ny43MzM1IDExNC4wMzJDOTcuNDM1MiAxMTQuNDQ4IDk3LjIxMzQgMTE0LjkxNCA5Ny4wNzg1IDExNS40MDhDOTYuOTI2MyAxMTUuOTY1IDk2LjkyNjMgMTE2LjU2OCA5Ni45MjYzIDExNy43NzRWMTc3LjcxOUM5Ni45MjYzIDE4Mi41NDcgOTYuOTI2MyAxODQuOTYxIDk3Ljg2NTggMTg2LjgwNUM5OC42OTIyIDE4OC40MjYgMTAwLjAxMSAxODkuNzQ1IDEwMS42MzMgMTkwLjU3MUMxMDMuNDc3IDE5MS41MTEgMTA1Ljg5IDE5MS41MTEgMTEwLjcxOCAxOTEuNTExSDEyOC41MTVDMTMzLjM0MiAxOTEuNTExIDEzNS43NTYgMTkxLjUxMSAxMzcuNiAxOTAuNTcxQzEzOS4yMjEgMTg5Ljc0NSAxNDAuNTQgMTg4LjQyNiAxNDEuMzY3IDE4Ni44MDVDMTQyLjMwNiAxODQuOTYxIDE0Mi4zMDYgMTgyLjU0NyAxNDIuMzA2IDE3Ny43MTlWMTUwLjM1OUMxNDIuMzA2IDE0OS4xNTMgMTQyLjMwNiAxNDguNTUgMTQyLjE1NCAxNDcuOTkzQzE0Mi4wMTkgMTQ3LjQ5OSAxNDEuNzk3IDE0Ny4wMzMgMTQxLjQ5OSAxNDYuNjE3QzE0MS4xNjIgMTQ2LjE0NyAxNDAuNjk0IDE0NS43NjcgMTM5Ljc1OCAxNDUuMDA3TDEzMi44MzcgMTM5LjM4NUMxMzEuNzA5IDEzOC40NjggMTMxLjE0NSAxMzguMDEgMTMwLjk0IDEzNy40NTlDMTMwLjc2MSAxMzYuOTc1IDEzMC43NjEgMTM2LjQ0MyAxMzAuOTQgMTM1Ljk1OUMxMzEuMTQ1IDEzNS40MDcgMTMxLjcwOSAxMzQuOTQ5IDEzMi44MzcgMTM0LjAzM0wxMzkuNzU4IDEyOC40MUMxNDAuNjk0IDEyNy42NSAxNDEuMTYyIDEyNy4yNyAxNDEuNDk5IDEyNi44QzE0MS43OTcgMTI2LjM4NCAxNDIuMDE5IDEyNS45MTggMTQyLjE1NCAxMjUuNDI0QzE0Mi4zMDYgMTI0Ljg2NyAxNDIuMzA2IDEyNC4yNjQgMTQyLjMwNiAxMjMuMDU4VjYzLjExMjZDMTQyLjMwNiA1OC4yODUxIDE0Mi4zMDYgNTUuODcxNCAxNDEuMzY3IDU0LjAyNzZDMTQwLjU0IDUyLjQwNTcgMTM5LjIyMSA1MS4wODcgMTM3LjYgNTAuMjYwNkMxMzUuNzU2IDQ5LjMyMTEgMTMzLjM0MiA0OS4zMjExIDEyOC41MTUgNDkuMzIxMUgxMTAuNzE4QzEwNS44OSA0OS4zMjExIDEwMy40NzcgNDkuMzIxMSAxMDEuNjMzIDUwLjI2MDZDMTAwLjAxMSA1MS4wODcgOTguNjkyMiA1Mi40MDU3IDk3Ljg2NTggNTQuMDI3NloiIGZpbGw9IiMxQTI4NUYiLz4KPC9zdmc+Cg==" + }, + "b84e4048-15dc-4dd0-8640-f4f60813c8af": { + "name": "NordPass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSIjMENBQUFCIi8+Cjwvc3ZnPgo=" + }, + "0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": { + "name": "Keeper", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K" + }, + "f3809540-7f14-49c1-a8b3-8f813b225541": { + "name": "Enpass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0iIzBEMzM4RiIvPgo8L3N2Zz4K" + }, + "b5397666-4885-aa6b-cebf-e52262a439a2": { + "name": "Chromium Browser" + }, + "771b48fd-d3d4-4f74-9232-fc157ab0507a": { + "name": "Edge on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=" + }, + "39a5647e-1853-446c-a1f6-a79bae9f5bc7": { + "name": "IDmelon Android Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=" + }, + "6e8248d5-b479-40db-a3d8-11116f7e8349": { + "name": "Bitwarden", + "icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K" + }, + "fcb1bcb4-f370-078c-6993-bc24d0ae3fbe": { + "name": "Ledger Nano X FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "9c835346-796b-4c27-8898-d6032f515cc5": { + "name": "Cryptnox FIDO2", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "c5ef55ff-ad9a-4b9f-b580-adebafe026d0": { + "name": "YubiKey 5 Series with Lightning", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "3789da91-f943-46bc-95c3-50ea2012f03a": { + "name": "NEOWAVE Winkeo FIDO2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC" + }, + "fa2b99dc-9e39-4257-8f92-4a30d23c4118": { + "name": "YubiKey 5 Series with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAfCAYAAACGVs+MAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAHYYAAB2GAV2iE4EAAAbNSURBVFhHpVd7TNV1FD/3d59weQSIgS9AQAXcFLAQZi9fpeVz1tY/WTZr5Wxpc7W5knLa5jI3Z85srS2nM2sjtWwZS7IUH4H4xCnEQx4DAZF74V7us885v9/lInBvVJ/B4Pv9nu/5nu/5nvM556fzA/Qv0Hb/IrX3VFKPo45cnm4inUIWYwLFRmZQUuwjFG/N1iRHh1EZ0NRVRudqt1Bd+2nSKyS/Ohys0+lk3e/3kQ9qvD4ZUta4VVSUuY0eipyiThAfocoORVgDuuw3qKRiAd3rbcEtjTjYIof6WaHsCmzVPWCMx+cgh8tLqWMKaMWsUjLqo2RtJIQ0oOzmerpQu4esZgsONkGxH7d0kdvTT17s4OMU7VI8ZhjgGaM+Aq9iENu8Pif1udz07MwvKWf8GlVoCEY04PC5WdTaXYFbR8vNvL5+3Kgfb5xNMya9RamJiynaMlGTVtFlr6ba9u+pqnEX4uMuRRgjSYEhrN7utFFe6lqal7Nfkw5imAGHynPpbk8VmY0xstnptlFCVCYtzTuBN83QpMLjTtevdPzSUnJ7e8mkjxZ39fXbKDfldZqbvU+TUgGnBVF6fQ2iPHg4W16UWUwvzbk16sMZE+Pn0pvz7JSeuAyes8lcpCmaKuo/p+qWr2UcwIAHWrvP0YEzhXAtLAbssHhp7iGamvyijP8ryqrXUWX9XoowxyAufNBrp43POBFXZlkf8MDRiqcpyowAwpuz2x+fWvz/Dtde9smszygtcR6C1wbdzBl6Olq5WNYY4oGathJMrkTEx0jARSHAVs+5rYkQNXb+QgfPLsQ6gXyInsreQfmpm7RVFYfL86n1fiUOkYvShkUPxvbukzoy6K1ihM1ho3XzW6EvSfXA+dpiWGaWd+doXzLzmGwKYFLCAsRAlPBAhMlCFXU7tBUVPr8HgVcJHWq+F00plr+DMTdrP4zvxY11kNMhxT+SeTGg+d4V5LQJityUGJNB8VFZsjgYBZM/II/XCTkj0qyDOpF2AVQ17CIjUp/DnT1UkL5F5gdj+sS1wg1gE3gigm60fCXzSnPXbyAPbIXv+IDpE16ThaHIS9skyhlmME5F3cfqAKhq2C0E5PH1gYaXaLPDkZG0HDJOnKWHp51I0z5SOux8e1WAuZzdHQrTkp8TmjXoI+la0wGZszubqbO3ifQ6A/W7vVSYsV3mR0JKwkKc4WHiBkmR8I3CCgI87oOL4qzT5P+RUJBejEOgAPK8hYPzatM+eITp2IO9yTQmeromPRxx1qxAcsile/ubSeEbcWQGYECghcLY2HyKjogjH25hMpjpUv1Ougli4eh2eRw0O32bJjkyuCgNzg0vzlYMSiSs0uoo4MG7hMOjCEaX1yFE0nSvjBzuTnEpK86Z8IoqFAIubw8kg9ArEaREWSZI+jH4Xbp6g9E9EnJT3oaRzDN+MUJBQDHn56a8oUmEBusOxBs/N5+tJEbPkAFDj8UGvOs/IWvcSglGBhvS7/FTYfpWGYdDY8fPAxWSA35sTC4p4+Lm4AaqIoPeQtfufK6Jh0ZhxlbsUXOSmXNifD5ZTAkyDofbbcclxnA8WNAqxCbRNykhXxQpaDw67fXUYbsiG0Khtv2oeIvh8rhQMYOcEAqXG/eI+zngOc5yxr8q82IAM1c/FLFOplqu5eFQXrMZzGcVCjYbLWG5I4BT1euRrlbxtNOtMitDDEhLXIIynAAvuOEWE3X3NdAft94VgaG42XIQt0ZX6PeCE/qQFe9rK6Hx7YU50KvH7fW4fS+q7KKBJxsggBX5pSAGh1jIrVh5zQ6w3RfaahBXm/aCbCZTjCUFUTyWZqW9p62MjJPXVqOrPgMO4Nv74Gkf+owftNVBDQnjFJqHSw17pXvhWW5KZqe/Q49N/USTCAVWoQXFIHBHXXe3FPrUDsuGDmtF/hHKTHpekxhiAOPI+SJq6S6HF4I9YWzkBJTo46iUMzWp8Pir/RiduLxKYsSksV8vLlOQvhGX2YlR0OBhBjC+u/gEcvY0ApK7Yk41NxjPSQnWFHTF66UrjgevB8Cu5a+l2vYSRPtuVDo73hhdMSHnUX7tTjsVZGxAl/WptiOIEQ1gnL29mX6/tR1tmlkYj8W4X+CSjWcUDGY1NpS/C7hSKqiMLM/l2QmSWZ73Ddz+gio8BCENYPQ46qnkzwXUbqvBkxjUQsWfZFgbuo3rAf+wN7jOO90+ynx4Pi3L+0nYL1SchDUgAP4gPV/7Id1q+1HShmuGkIqWRPgyxMFqP8HfjTnjXwY5bQfbJct6OIzKgMHotF/He1egsaxHSqG6wfdmQ5x8NyTFFqBcp2iSowHR3yk5+36hF7vXAAAAAElFTkSuQmCC" + }, + "69700f79-d1fb-472e-bd9b-a3a3b9a9eda0": { + "name": "Pone Biometrics OFFPAD Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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" + }, + "89b19028-256b-4025-8872-255358d950e4": { + "name": "Sentry Enterprises CTAP2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAMZlWElmTU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAExAAIAAAAVAAAAZodpAAQAAAABAAAAfAAAAAAAAABIAAAAAQAAAEgAAAABUGl4ZWxtYXRvciBQcm8gMi4zLjYAAAAEkAQAAgAAABQAAACyoAEAAwAAAAEAAQAAoAIABAAAAAEAAABAoAMABAAAAAEAAABAAAAAADIwMjI6MDM6MTggMTQ6MDU6MDYAc0fjyAAAAAlwSFlzAAALEwAACxMBAJqcGAAAA7BpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WVJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDAwMC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjY0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjY0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAyMi0wMy0xOFQxNDoxMTozMS0wNTowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdGVEYXRlPjIwMjItMDMtMThUMTQ6MDU6MDYtMDU6MDA8L3htcDpDcmVhdGVEYXRlPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPlBpeGVsbWF0b3IgUHJvIDIuMy42PC94bXA6Q3JlYXRvclRvb2w+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgqKY7VlAAAE7UlEQVR4Ae2Vb0jdVRjHz3N+V+/VXZ2VA1PZDGSRwgpDyFejP8ygIMhFFGU52IKVSLTVLGiXijZqzSFWQ2KQNNZ60YuNxdiYjv7QQHtRU7YZadZyoGZcN696r7/z9H2u99zd3bS91p0fnHvO7/l3nudznvO7SrnHEXAEHAFHwBFwBBwBR8ARcAQcAUfAEXAEHAFHwBFwBBwBR8ARcAQcAUdAqUjPcOgrZm8ls9CLFcdKEUcimvxr/RfO9HdHegZKFrNbCTLKLiLWVlmnPXMG8lyMRz+o/roSTXAAhqeNF34q8uBds9k+y/k9DYA/raiIJ7wjrPh2rfh5Zh1j4iMozo8G1jQeXP/ZFkWqIe/it7Wx8fHJSCQSX86F29zTV2A2oX80xJ1eidlGpdzllZk3gs1DG4hpb+H8RPfb3zfvIFY5mgP14TtK2mwAOzOIZY0k3CxZ2kb8oCPR2xjZsqV8rc9iehsLuv+Nbe3Sm5vb/JrAnaaVtDrGSj/nNQw3EikOtvz2ZWgyVKp29/jihKi4ArrcBsBGVb7vzxjIs8afkgRk17LkSbs55mpjTGtKd0KKScV8QmTyvpQv5BPQl6V8b9jXN2YaurUYR6GP2Txlxn4t4pMps5uqYB4N4eP38YehYxW5m4f6pHhrSJGBOBFKR/0ofgZnV2R1CPqqIgrBoHKWqEKGJlqHcZ/4wC4H809Wl9KvxQfmEnSyv3RFDRIbxVwMyiLLwWCJEyMq94nqRGaIHpZ3jLuRhHSYyB5PycrniWogyzdKbUesOayDmNdhlm5bxUSbsEwepMjkSQOAMnDCa/k8HqKzSc0iP6QoCjvsrQqsWmt9Vta5zBdCzIMyUMwgwOzFppKkbF5rdTL7zB2AMi86PPGpaPQekDoPvxEyZuOCGBUSjRcQjYHG3yJDYZPyjjFubQJKFQcTiVIZgLomKTeGY1q/hpg9iDk8b8wPmMcA9H42ptH6ygx/+7A/Fi4rxLZL/u97Vy5vJPaPsqKr1gtJHka/9gZB3sqQaAPWb2LsFBls+kHiRVnLg6OZWlgt/I6Ojsaqioo2IdFdSPL9TN3N1ijssBdYKANrMffntP5EQGH9EOBXQ34eSexCDvsDgYAFnwyd7gA0vB/SaA82ur29XVoneXpihS9+4KOOztZQdGQE4u/iFHoy6Y2feebNOcwD2KTXDhz5W1AJpBiCwIfvha7P6mF/SRKzMWQGJM7xvD04oS2Z8putAazW05pkIEiX2OcpNZHhd1nWQDOMPW4oXuTpDkCi/+T6Pg6XKOGFntnX0fnyvvaDL/Bc/ggFZ84ZVn9orepx8zqDJi73N7kZ2qUPd3SrBMt4eHZ6+mQ4HMa3jtejI56GDk1y/YlGo6P5q1cf174/UlVVlU7M87xuXNImAXLdWk3g6jWhxf+yMhQ5iX2b8P67leE0X4GsB+/lGL+m5DMCFXF7rV3mnD5l/qKysK24K3DVKxhGMzxWcPFcGc7lEAr4xqdEi6dy98OxHrJndzRvPYk5M8HMmMtqnQZgs37v9M8PGO2dwnW6wvNTdasG+1/HYezEyR8a/EVt7+x8KWFtV8L8HwC2qHe6B7ahdfbg9hzY/ciGd638lpojx/vyIz2c/k7cUsW7Yh0BR8ARcAQcAUfAEXAEHAFHwBFwBBwBR8ARcAQcAUfAEXAEHAFHYMUR+BepFtGiL8LYmgAAAABJRU5ErkJggg==", + "icon_dark": "data:image/png;base64,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" + }, + "4e768f2c-5fab-48b3-b300-220eb487752b": { + "name": "Hideez Key 4 FIDO2 SDK", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6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" + }, + "931327dd-c89b-406c-a81e-ed7058ef36c6": { + "name": "Swissbit iShield FIDO2", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "8d1b1fcb-3c76-49a9-9129-5515b346aa02": { + "name": "IDEMIA ID-ONE Card", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "454e5346-4944-4ffd-6c93-8e9267193e9a": { + "name": "Ensurity ThinC", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHoAAACoCAYAAAAvr/rAAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAG7tJREFUeNrsXQmUXFWZ/u99r15VdVWv6e7shCwQoixBRAyDIjCiDMIojqODx8OMo6OeMagzOuNRORJAnUEcZ0ZI4IAOyL4GSAyMHDiDJCogoECTrROSkK2TTi/VVd21vPfu/P9bku7Ke69rebV1vwv3VLrq1a177/f/3/3//25s46ZNUIU0kzH2OL42Yc5BkCiFMe8FIS6vRp/IVWqUgnk55kiA74TUjZlV44d4lRokMGcDXI9LmWr9EA/6enqkAOgA6CBNpSTXST1o/NanYP/qVh8rAdAA29HF+Di+JqYgw6jA2DJ8fQhzx3QH+gh2xmbDMhdiKrJmyso1BboeNEiy8tRLjNnKVHOF4nXYMVMJ5MDqntJg12EbeNBR06PuPOiw6VFnue47zs0SL6RTi7XiCwXKqdw6F0y5YbSkFNdrfOeXIzCTldsA7DN9QqD5YNDffgDUIEOM3BC19CuQMpXctymn0VMzWhYAHaTpSN3u2kwcTBMhs+D49VYhzHdjTpb4q1TmJ61yx8+o0TKoVzD/LgC6eoli4zdgfpfL50+VAfQpmG92+Ww15t+DuTQqoO5q6DrmEY/P1TLKTnt8lmzQ/grG6MAYC1IAdJACoKuZYhVql5eBGm1Y94q1tlg9w0AXtJrHit0K9CxG6tr2eMMCVHcAqpwtLmOYtzgYdFTuvnp3R1k8DkKSgCOGbFwkUE6vvs1wSlO6BlElBE2yDJqmg4hGgX/m0wDZLHZbjbZLuc9eafju31tulnDwsVOexXr/6mtY4NkO5XKsS8bNtWK1DK9ylPdIBFgoBLnH1gLrOwTDuQzoun60XnL6zrsMtRhQc9ARa4I4AqyqKggEXH/xRYBLLgF+wYfMDs/V1f64sQqVqzeEG0V4IEYQDgMcPAi5//wvA2j1T68DT43B0GA/qMjK7Ch1t7QYf0haDlhTkyEZqNLYXGxvby+wO26H3L33gvTd7wI/aQnqSqq6kwNec9LTOSkKivoYiJ07Qb1lNYjeHSBCCDxHkmtrBa7nEFP1qEZzzw5G7TY6eXAQ1GtXgdi+HaCzc1rPAtWctgmPWMyga+1nN4O+8msABw4AxGOmknJetIV5DHCih5ERgBt/DGzZMhB/exVAc3P1qNwPraYOQCPFYCsqj/72of6s2uyGIIsHHgTx6qugv/kG8BkdJvtO0j+8YCki7R44AvDMM8B+9CODNgwBqHdapc4hoSSjcs8e4Fd/DdhddwEMDZkaQLkRhgYSTLSo4f77Af7nToC33jI1u8C686IoAwd7aG0F2LoV2PU3mON1pIp728mGKKa+JJyY2caNwFavBr7yamMYgl+jsH7xHwDWPg6wbRtAV6cr5TmWS8yA/UDGT9WEhNqOthK7514T8GL6ouTAAv4QQ7D5DRbY1OBKayUaH2ztWlOwyNokYNwyAdHWBuw3LwC78UZgN/wA2Esvm0JKn9H3yR25/XZgN/0E2E9+CpBImJ3nVa6tVYcPG2WStVsVQUfNZffdD+ze+8zfL2G4KG2akqSYfnDLNuDXXw/6NdeY2kP06PeYRcDgEMFWrwF4+ilgqI3iik8AnHuuOXw4PT8wAJyeR38SBgcAWlqO1Xt8am83KfzJJ4HhmAennwY62R9pl4MIUKA5GkAGC7zzDsCuXSBWXYuM0GXWxe+2k2Chy8vuRbq+5x6TqkvVlQ2z5hpq3Y/uVRtKdCdKaBYNFoqQMew0BgJUXYBgHELcjKCZ/1uNQo0WS5eCuOZ7JvjODZ6HRksPvrY41AFVDc4Fp6lFIxCAArRmDbB1603LEn18QyMlyVsQSehszS8kkWFG9Z6MnWxhpueorbNmgbgWwZ7ZDTA66tT2GVYUb7ZDae9gXZeA07EfVA71J2oxQ8o+yjbYNp0CWpyBhn5yfkRTVzV8TIK+gX60Owtxr4qhcZRwTlSWTJr+nV+JNJlA3rABabfFbCiVbzXYNdt+Ji+ieQQcCYZXubadYj9HtI2uDSOg+/pMVvNxTKbxmN13n2lM8vKgKh9o26/bsgX4ddebUl6ukULaSkbUbbcBrFtXlHVZ9USatn8/gr3KGLsNT8QH65rd/4BhfBnl+9B2f2avbLBJs8kaT6dLd1sIZDKUbkWQn1xnSnO9hyKp7WiYse+jZvf3Hws0legnAxpeBsj0b5/GfX+nKaliW7eZmk3WeLHSbQdnkK7hV78yjahGiMLZNG6DfRBpvClamsDYdE3jM/cPHv/no8lgIteLNNum8UJBJk1ecyuwJ9fXvyY7JdJkovFVSON9RdC4ZXiZLtS9ZVnX1QPadr2IxlddZ4JNhpEbldmzMORG3GqNyc3xxpzIsLWSDLTvf//YmO3VdvqchJrG5DJdqOoCbTeAjAiKoBGVjYzoBpXR+JufSQgURYdbVjcWXU8WkSOwv3cNhY0F/q07tj0c1tEfF/CLXxwLhnDeQECPd73eeAPY88/LqLW6MaGQnznnSFcSe/iRY4GNqZDCEZPGN/5WRlC5S9t19txzErvzlyaLVVDACw2YoF/O58kMzpI4p0Xz88E8qXfys8E0LLi5eQF0dV0Aui5PXKBhRF6S8Paup7CRSQJ9ygBNzdS1LAr7fGz7hfiGchyFM55Fel8PyeQQCoNUoGKOIDa7dCF6NKG/isD04b/FZAGTyUKgKJbsYkXi38EvnMOghC0KVH+KIw8MuOo9UvengE+xBanMajtN79JEinNS0Fi9AqTiDmUi8CTKiDuCvA4V6N80IV4SHhsXZNeyOF+B2vvfWN+zxgtpSYmMLVmGaZkq3HbO2GVcki5DrNbldP1biPTWgsZoZAD8Dr9GlqRN40EOUn0nxOwyRZJeV0Khv2aTaTTyPGec3YS8//Wg6xrN2Df4VgnJ0oMqgy5k9ltoFaht9hwFGrmeKbK8Ckn768FivMb27kKSfHNbvPlQYnT0YVrfLWyrmz7lYeXClnjTs4h4QXZGWWN2kEq27wrpd2sKOZ3MZt6dVdWd9JeclSUIM9bcGo3ewYS7v8SOaf5G9ACfxL93MfOqgGBJaHUSLQFsQp1chl75JxHMU91AF/gfWuaRqCzfkVPVP0da12Uy1WVF+SvO2ULNhbJJ/VVdf1kV+pe5pv8JrUgtQLdGrjnFNzT9h4Lx8xGX22XOFzjhJgyrWrogpigrhKpt4lGJK4osfVu4gYw5p2l3j2ra+Vl00BFzLejympN4VtW0Z4az2bMRl99yl4gaaXaIS/8qSbyZRyS+CKn7ZCfKJqd8TNOe7Usmv4gW3BgLWLp+3CliWSEO70sk/lLVtT7X9eUczgfGz+E64+c5xVitKFg6lcuuRJ7PILWbO/SCPq4fo4zw4Lw/oWlfcseFtQiJXchDknSu7kDbJCFpTV2fUnNbkewhlctBIpOBHFnoQV/XFOSspkMGMxMMWsMR0FV9A2r1djetlhlfgErKFrs5ZKqmP8GB6+GQAppOkxs6JFQNRrLBXWW1NMgymkC7CcdfMrgQFxn/FDrcJ9zjHzSb5HxygDA3Um9pylshYiwtDfq7JppM1nVKU62LKdgEVzgj9C1xJoHuArbsHmER0CRJuSaXmRURBEyqngis1pDsFiRJe2i09zSlKEDKArCrZ3xJ3gsTmJhESMquRAB29cbnkt0xPyUuSJXT5rL97nqrUJCO9aeffcrrUfqC5H/ilZDEINVfH/JGqWhA13UIdAD2NKDuAOz67Cve6A0IQK4ToAOwazMm56eqrqrnQkDIPtCtgikrSQVHkahOCm1B8qoTnRvCOWS5cYILhLXCFtlkrHootOyWVnxZvyGsOlYzVQ1oGRtKjdvS2QmSrpdUhm50Fphrld12ouLny/qPGACmjb1j7imWy8FgJAKbO2dg/dxFQ0OAOtJpWDg0ZLShh07rMxZhMFdV5Vjesv5+o9072tthOBwGiWb+6CQtTYeTBwYmrV/DAc2tDWBPLVkMPV1dRuOLTTQzk8lmjA1kYUUBt5ka6sje9sOwaHAIzuzrgzFZNrRVjKNLqg/V4Tfz58Oe1hbY2tEBisfMj8oZdI2OwtJD/ZCWJXilu5tWdrjuCaQ6kDC/g2WHENSerk4YjEaMuWNhCcGlvTvgDKxfukpblSr+K5LVgY8vPdno0OZstqTgPAHLjZ2CSLWcg9eUHAnT221t8OrsWXDR27sMkGx6VjH3IKu8jJ/1W6fvtUxyPhp9kgwpsGneXOPA+gjWg43bBeGW/jB7ttFWovpYNjeBITag0FPByw8g2CG54hNDcqXpmjrXBjlK54BXQXrpd2hM7YvF4P5T3w1zRpJw/u7dkEFtfGrRIkOLqLMVa6wVkxhFwhLYqLWduNAUUVVX4SdB2bB4sbGzwmaehgSa6JGOBX8CQd5mgVxVi1aYS20oHYzH4JennWq8R+BS3XiNtx3ZRiANZyRkZ1hgs0YCmhpBnbr2lKWwvYqa7FWfSJUFrRjWIxqn/jnTGrMr0Ve8EhUnqXwcQSZNjtQY5HpPtmH4NIL92syZECn9DHGvU266fdVo2314vEZ03chgc4vGKS0vjcbpctRvwcT7QJglAH+U/awsWRa2JkcDTS6JDQ2wsR+X9x1GsIsIqjDYgV+8qaLULVl0vdayrgO6LoPGMT+x4AR4sb0VmnxkRNkPkMkHXRvQtW/DH4VMH6BbAVBb3j88DCkfImi8XAk0/eSTDOu6KQDZH9cQzPj4Y3Nnw0sd7dCklb+BlZcjeVQh00+eEYzJFQCb+vhRBPvl9jaIIdjlaHVJ1G1HvMbTdQByZWic4uOPzp1DZxjAOQODMOpgoFHf09geJhzcNtqVImk0g7N+8eKjwZAgVdZAC2MmGqfOX3FkwLphddwFo0KHw7Em2DRznuuEkVzKDw9EIrClcwbEA02unuuF/b5+Zjf0treDFFYmTNMSJgl8b29rq+sUcFFAC4u2NyxeFLhQNaBxMsm2tLVANBI9bnKFwI57bGcuCmgCebs1iR4sD6oBjQNNeermlKvTpIzHVGtRVjfNq74yayakQiFj0iJIjSUkBSXykf80cybsxnEgEhhgUxNo4v9USIadba3GhH1A21MUaDIEDsZi8MfubohowTFjDWm1F+NWhShQcuytVszXWQ6dVkFBpIVd/84Y6+XmgrxFaIh8D98bznsujplmbzaX+FuXYb4Uc/6Fl3Q1379gTuW9fybmr2EezLeXrH7Zn/f8HMyrYOI0olOidjyEbXymyPrT/UvXg3GYPuTG1YeuG/q/ciY1qICrqySQD2HutY5XOhHz37k894Qb0PTdSdZ7fQTzl1w+u9YB6NMxX+Xy/B0OQM/C/IVCGhsKhb4QDodPw/q+OX4BonHsI01wOLeFAP5nlyI7y5nUIC3WqwT0+OtfcwU+5wi2R0p4fObUzjGP553qWJAFa9xlgvVsbW19TJbl5pGREaCcSCSM3N/fD5lMxqkthPyoS7HD0+7+A7uDRB27hxbYJ0UikZ/v37//b/BvTbNsIxU9nng8DqjxRbWhHI3mAFU7RDA03QSSQEQwPzVnzpwvTzCqZBn27dsHuVxuMoYqzRhzoe7dHtRm01U7jREedNnnUQ9uGUPpSmi2qPOgD21Bamtru3l0dPRlpO2XCGS73sXeHFUw0MZZ0RMliAA6FbxPoKL3V2B2syDJSlwD3uvnqUWpSoBSJzSOeOp9CNzsfOGjfxPYnZ2dj+Bny/GZATLG6P10Og1N1k4T36hbRemZP5KEc/YfgNFjR0YKy1UYsV6dMlmqvR5F91rPJD0yab2mF7ZfqxzNz9TIZuDZbPafUGtXOlExtRs1eX5HRwe5XDL9TUDTWF2MVvNCtZn86BOHh6EFLT69uG2vEY/Pwj7323usoaIb88wCcxd2ML2eXCuVVhSlHzX0VrSmf+MEHoGLLtdFaIl/0zDpcXxGwTByoeN0wSJBWzzfhab93GSy6nt7tcL3PP0H5gFrWDlYYD5kvV5ZK6MLQWwj4jxy5Minqf5umo1W+A9QKD5IAB8+fBgGBgYKvOmwCKDpp2nW6oN73oEY7Yis0o2wWpEb2xoxURvR6CLQDg4PD1/hpqX4HEfXah0CPosAJr8any+Iwosy3YiyO8fGDK3WA5B91WoCi8BD+n4eqfk7buDh+y1dXV0PRqPREH3PDrD47kcT2Bft2g3ZCu78m04gjwebrGgCbXBw8MdooK1zG6/Rv/7g7Nmzr21ubmZ2EKUiQNNm9hV79xnjdqUaLabZwgYrQGJoNoKpItifx/cOuY3XKBTfQaA/OjY2NsFV9C1gYm8a//CuXTCqKPBmd5exzLROUj/mvSUGgk4Ec+aopikWixlGFjJa/9DQ0OXoVv1ey5satpUAx/XHcJxeip/vQdoXJR/M7qXVBHbetGU9pJXY2EehhFOdUCNoinNlPRhmZGDReJ1KpV5E7V3Z3d39s1zedlprbI6gcfYQWut/1tLSoiIjcK/7y0qjGqjLA9kPYuOpR7Il5IF6oW+ywO3AyN69e9ckk8lHnNwoa9ryHDTMvo1CEcW2D/s2Rtd5ihYyXrnFLeqhARTPRuDGx7M11NgvoqbvcBuvke6vQ82+CZ9RpwvQ+jgqblifOhQKGRY4vSIlA1rgOFwPXeFlVOOzV2Gb504XoPPH3YZ1syKRCNjxfWoH0vfrCPjni521mhZANyrYdqRMUZTxgRIKptyFYN9RCtjT4vbBRgSbaJsMM8qk3ZYVriOFfwMF4a1iwa410PWEQF0JPVE4GWXjhZT+jWAn+/v7P255CgW3razG6ePO2CyxAyUvLXTJkp8BoAK/y4upu8vzvJjnbaCd+iWdTm8fGRn5XBFaHSq5Y2j2qonCb+k0ZK0TiVwA24ouwwwHt9s4YjOfVomiKGDQ29t73BQcjV0dHR0bFy5cOENVVeHQWQnXw2AnCaliPa7B/EOX8MCQwzDwKJbZCccvo2L4/rDD82+AuaQq/3kKcowUU2cqE8frh5DSf419xCZpG1UgUzLQFOe+cN8+6Glvg8GwAiGPU5CLDUZQxSkSlL+qhIC2fMVKBDfGwHsJb34i6jxSjI1V5POF2B1DVRmXckgd5x3sQ5GsjAHllINUAwOE8D1laAhiwab4qQ00GWN0hsnF7+yFJO2ZDvqzblPZOzWIvueMjsLJaEDticeNGa0gVdENIwwKuEOkbKDpRPtutL6XJBLwdktLALTPIGbz/Gj6m2MO0S0E+Epbmi/Ztt1YDOK1OteX+6NHkLZXoFHW094OB9H3UwKwfUkagrk4nTFOTDb7mtEEh3Gfh5TJwBmoXNTvzDrczyv+5OsmuzOOHIGBOXMChHxQHjrG+f3798OHDxyYqKn2vw/3G/udaHPFsUNqfDqsZrKx+r2HDtfjqpOGo+tRC+SLdu+GMRx/09i3RzNFxijjv1W/d2oUI4kf273H2LYTWOClgZymYXDffnOlLZd8UxpfgdYtw+yEZNLQ8CAV33/n7t0LF+7ZY9ytofsYIPJ1jNYsoE/HsfrJBQugOTDKik4rkLJV62rEItM8/NJFcPxGQ9pyudn3Ew9SKInL+4/AZrTAd8djENGDEbuQIY/67dKdO42TAUsE+nzMd7p89rTv/Eq+HZ0wuGR4ODDMiohFzEAmnIdDXhlkPerx2UBFBlKa2TrvwEGI5rSSN+P5LSB1ujzZSORKnYrD3bxExWwbvSKlMsu/uxgNC7XIilNDhUVdfpxnQeUZ1xPSHmPM9t91o81YH9q0eGbfIfRWKnd2UMVKJu0h67s1m4VUU5NB52ISwSAmmJVMwYqRETgPx6qn8b2dCApVMlKsoJGviZoyK5WChUPDcIFxN6UMGxYthENYn8PRqHGtcK0hpzupT8D2tmUyFb2fsmIlk2tAB7d/dts2eGDZMkiEw0cv/cxPSUWBmSgUZyN9nY0+JAEgEPQPqxo8JnH4I5b1Mp29VeBv00xaNwL8vv0H4OwDB4xzrAlk+v0r39oMu1pboadzhnHkJXV0IcdeEgtkrNtqCj301rgy2LJZ8u/CtKNfZ6EmX/z2rsa9hJRSBjtxxlgaPrN5C4J9igGolNdgGsMvRWGYmxiB+YmEsdk+M24J0WdRsz+Ar9vxuZ+DeTi5cR+0C/2SQ/ex3h0wF7XkBLs8qxOFJQSzUajomI5lRwZge3sb/G7OHOM6A7fzronuF+Dz59J0rBKCpxctMn/fow4hBPeTW7YaIL+w4AQ4EI9PuAaB+mY5gvwXO3ZUJeZQUaCZZZi1p9PwuTd7XA8MbcXPqeMSKAj5XUdrhuhQkg8gEHTIiEBw5KE/eBpWrRnz3Bmn8uxhgjIJwixrfJzsWmECLG7NEM1DoZzMyKSJBqoHaXL36OhxYNL3acEG1UOvgs1Q8ZMDmaURUY9rAHKWBjO38dbKBnWTyzYJ1dqdOln32VTcnJ185aywqRhTrMCLQu2zXhQENOziimpViiBW/ohIa4WoH1J7FF4fNUBYHV6s/VFsbEF49U8VNJpXBeggefdPFULFlQU6iHXXjULwAOTpodk8ALnOUoX6jgcgTw8a5wHI04PGeb1WLEj+ajavtwoF6fgYhB+p/IBJoMlV0epCNhiKUoFm4BxGtCfxBYIcrPasDthHF064Ay5YKUAbO+tVVR5VtQlla3SIGb7R5jBhEKRKgk1TpQLGcqpjv8uch5tCIdBdqJ6Adr1iQBViSULNvcK5dfeELiAiSaAES3lrgTMqGIeoJEClBZdiIlmj8i3xOvmA67q+w3HWiDSX88uFqjF9LANaNgdc4hCXZWhGyQnMrxoijoBlVRVyOSurmowMfKXHOC5x1NoXnUZiMrGiodAV3VLoxJimQxx/oD0cMbRZD6zsmibCk84gE4oMuowMK0kfCknSu9xwQWXezxO69oIbEaNWRyKxyE3p5lgoGo/RVTWBJtdJouGUpj91zpoVJXSrxyE9o1khnuNNjG9Thb7bURLwy7IkXYGG109RiEIByPXF4CFg8SZJfohxvtjjuY1ciN9ypmlpVO2b3PidwG6V5X8MS9L6MBYoIDC268Dbor3w741K0qYo5x8FrzO6EVvQtSH27IkLCbq2qBLZgl+f6RWJIRsAgf9ffOZhHCi2M13PBd1eLQ1G3wqNboTndOz7KxHp8zypndxjTXstm8u9DxFV5aTQyW0a0tTcV5vD4Ye9THT8REHNvwwoG7acFCBQLQNsnCFWSEK/Wh8eHf0c3c8h03EYWjoNeiYDiZHEo9lM5mfBWV6NLxDGTTup5Fcy2WxPGt2wFF1vqNMSVloFmc0KbWCAbhS/O+iuxva9NE1bNTiSvJ28JG7dfyJPjG9qOaHrX0D67kdL7hte18gGqf40mbBSVfWriN8ac48Dm2iYTfDEGUO3S3wTuf0yHWBfQOQNQNVgzEG8hhp8dk5VbxEO93m7xUp0tK7X4xeXIuBfwS/2BYDXJ8iI0+uI0yeymrYC//0HNxtrsmnKlKrrt+LrPSgi70Gp+Ahq/ElYWIfhrwepFilDQysqXw/+++mcrveg75NmR6+ec07/L8AA1yQ2351WYN0AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,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" + }, + "e1a96183-5016-4f24-b55b-e3ae23614cc6": { + "name": "ATKey.Pro CTAP2.0", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "9d3df6ba-282f-11ed-a261-0242ac120002": { + "name": "Arculus FIDO2/U2F Key Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=" + }, + "fbefdf68-fe86-0106-213e-4d5fa24cbe2e": { + "name": "Excelsecu eSecu FIDO2 NFC Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "ab32f0c6-2239-afbb-c470-d2ef4e254db7": { + "name": "TOKEN2 FIDO2 Security Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAA+dJREFUeNrEl09oXFUUxn/3vvfmjzOdmZcmcSakmUyGqoQolBQXMV2J/7DulLYGFHFRN0J0IQhSUAp22Y0utBZLsaJYMGhATV1INxJr1ZKmNqUYM5kYk2kmMzGZmffvuhhJtULmjQ7NWb533zkf3znfd94V05l+gMeBV4F7uT1xCTgGjIvpTP9DwFdsTzwsgeNsXxyXQHYbAWR1wAaCvj8RApTCW9/ALZfBdRGBAFoijggGQalmANg64Pmureu4xSJ2YZlAupfonvsQwSBucZXq5Su4+XmM7l2IUAhc109KT2+muL34OzIcouvYUcxnRzCSyc331anLFN5+l5V3TiITcXTTRPkAIaYz/SUg1uigWywS6E2T/Xocra0NgI3vvseanSPY10t4cA8AxQ8+IvfcYbQ2ExmJNGpJ2T8Dmo5yXaz5BfSNCrnDL7L25TmUW0VqISLDQ/ScPoE5cgCnUCA/+jLBvt2tY0DoOs7KCgiJnohT+2UWoyuFCBgoy6Gau0pkYC+7J88jwyFm9u6jNnMNvX3nlgxIvwwox0FLJJABA7dUJtCbRug6eAqha4SzA6xPXaD4/mkAYvsfw11bbZhXNqVaz0MEg8hoBLxbxKMUGiHWv50EINiXBtwWA5ASZVko2wYp/+UPChstGq1jrVq+UurNGJCyLFTNQjkO0vMQ4XCdCSlRGxsoPBIHnwSg8sOPCAItBADYuTl6Tr0HmkZ+9BWklAjDQFkWXqVK6sgbRPY9gLN8g9LZMfTOzha1QErsXI7I0BDmM09jjhwgcv8gTuFGne5SmUAmTfL11wDIPf8CzvIyWmxHixhwXJRtkzx6BIC1Lyb445vzmxLTEgmsuXlWTp7Cmp2j/NnnBPqyLXJCIbDzeSLDQ2TPjQOKmcFhqlPTGLu66zMgBHgKZ2kJ5XkYqeTm0moQPpxQKbzaOuahAwCUPhlj/eIkoczdN6WoFEjQOtoRQtx81goVeJUKgVQPsf2PArB69lMEBgjg7zUUCNmcqn0NoVsqE+y/B/3OTpRlU/npEnrbzmb3/n8HoCpVgtlMfeVe+RlncQkZDrXsl6gxAFyM7q66D8wv4K6t1XdAi8JHJg8tYdbbUShQc8rwq3vLAPwztDYTvb0DZVutASDvCAMQfeRB7jrzMXJHdGttjY2z8uEZjM5UKwAoMOrHjGSSxKGnGvvWcoGlE29hkPr/RqRqNYx0D3pHu+++Or8tYucX6n/JPoxoy0GUkSi1q9eoXLjoG4AWj6OZJsqxG4pAb9QG5dho8RhaPNbUdPsoDmBI4Po23oyuS+ClbQQwqgMTwBN/Xc8HblPhKeBNYOLPAQDIsXqbsqZKGwAAAABJRU5ErkJggg==" + }, + "973446ca-e21c-9a9b-99f5-9b985a67af0f": { + "name": "ACS FIDO Authenticator Card", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "1105e4ed-af1d-02ff-ffff-ffffffffffff": { + "name": "Egomet FIDO2 Authenticator for Android", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa": { + "name": "Security Key NFC by Yubico", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "0acf3011-bc60-f375-fb53-6f05f43154e0": { + "name": "Nymi FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAALFAAACxQGJ1n/vAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAjRQTFRFKb7GKr7GK7/GLL/HLb/HLsDHL8DIMMDIMcDIMcHIMsHINMHJNcLJNsLJNsLKN8LKOMPKOcPKOsPKO8PLO8TLPMTLPsTMP8XMQMXMQcXMQsbNQ8bNRMbNRcbNRsfOR8fOSMfOScjOS8jPTMnPTcnQT8nQUMrQUMrRUcrRUsrRU8vRVMvRVcvSVczSWc3TWs3TW83TXM7UXc7UXs7UX87UYM/VYc/VYs/VZNDWZdDWZtHWZ9HXaNHXadHXatLXa9LYbNLYbdPYcNTZcdTZctTZddXad9bbetbbe9fcfNfcfdfcf9jdgNndgdndgtnehdrfhtrfh9vfiNvfitzgi9zgjNzgjdzhjt3hj93hkd7ikt7ik97ilN7ilN/jld/jl9/jmODkmeDkmuDkm+HknOHlneHloOLmoeLmouPmo+PmpeTnpuTnqeXoq+bprObprebprubpr+fqsOfqsefqsujqs+jrtenrtunst+nsuOnsuersuurtvOvtvevtwOzuwezuxe3wxu7wyO7wye/xyu/xy+/xzO/xzfDyz/Dy0PHy0fHz0vHz0/Hz0/Lz1PL01fL01vP01/P02PP02PP12fT12vT12/T13PT23fX23/X34Pb34fb34vb34/f45Pf45vf45/j56Pj56fj56vn56/n67Pn67fn67fr67vr77/r78Pr78fv78vv78vv88/v89Pz89fz89vz99/z99/39+P39+f39+v3++/7+/P7+/f7//v//////Wpo4rAAABClJREFUGBmlwY1/lAMAwPHfdlua2mWkFnVHShEqxIhiUipvkTo0RGJUWF4yUd6Z92rztqJSmBq2pmf3++c8z+1Wd8/urtun7xfPE1Zw6mB3V1f3wVNWgKUN7M20zKwlp3ZmS2bvgKVhCUOdy+qJmbCsc8gScIy+tiZG1ExNXbsgNbWGEU1tfzkGxgw+MYlIas3r3w6YM/Dt62tSRCZtGjQGi703i9C0R7uNOfDoNEKpPRbDQkMPEZr14ilLON1xJaGVAxbCAgfnA5NfDCwj2DoJuOaQBfCsA9OApUes4PBtwPQDnoVndCUhsSVrRdlnE5D83DNw1PcXQcMez+n9SdC431GYd7gZkp9Zhc+SMOOIeTgiWAQTP7Eqn18IiwNH4IiNUPuuVdpdCxlHYM5XCchYtQ1Q22UORoIFsCiwasFCuG7YCEa2Qd33jkNPHWw3gqHTM2GD47IeZgWGMPQaTD7huJxMQochDF0LGYsdvXX2q1aSgQWGUHug7pjF7gM6rOBYHfSoqI/BncbMBRqPWsGdsFFFnQO7jEkTWmEFb8FcFT1eQ+KEMWki71neiQQ1xxTdBdcbl4a5kBq0vOvhbUUfh3XGpWFvI2Qsbx08rmgrbDMuDd3tUN/jqKGjvXknzdkG9yg6Hz4yLg3dwXWwKGtO7/J6RtW/a+RDmK/oDPjJuDR0+3UCthv5YQoF1hj5EWYomoTfjEtBjz4EFx03dDvQNCXv6n1GjkJS0Tr425jBBjii/c2wUv0nQc1eY/6BhKIN0Gdk+J1teS/dCs1ZtRNqPtCfYZpxfTBR0anwi5HNFHrByB1w5ZA9kDLuEFyqaBr2GXmEs2oezho51ACb7IGUcd9BWtEl0GnkxMa1efc/td+852DCjz2QMq4TblH0AdhsWcE8uKkbUsY9Aw8q2g6tltdVCxsgZVwrtCv6BTQNW94aqIOUMdlL4EtFg0bYZ3l9UwmljPkOkoGiLoeMFewklDYmA3epqG/AZcOWl10K3GSx7Ex4S0UdmAx7rKBvNrxhsT0weVDF0FpYZCX/vvmpMQthrSEM9SbgA8flfUj0GsLIvTDntOMQXA0rjWCk9wJ43nHYAhMPGsGcNpjwjVXbPxGeNgdzTs2GK/qt0sk0XDVkDo7oboAlQ1blvxa4YJ8jMG8HsCKwCsEK4FXzcNQGYPmg5zR0D5BxFI7KrgJu/sNz+P1GYFXWUXhGcD/Q/IkVfdwMrAo8Aws8ASQe+duy+tclgCctgIU6G4HmV05b0n87pgPJdyyERXpvIHR5e59j/Nl+GaGFvRbBYsPbmwjV393xqwV+fe2uekIXv5K1GMb1PTmFnNSy9S/v3L1758vrl6XImbLpL+NwrP6t8yhh3tZ+x8KS9rctrqdA/Y1tBywJyxno6sisbm1paV2d6egasBw8T3ie/gevj4H2FDP02AAAAABJRU5ErkJggg==" + }, + "d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3": { + "name": "KEY-ID FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "4c50ff10-1057-4fc6-b8ed-43a529530c3c": { + "name": "ImproveID Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "ee041bce-25e5-4cdb-8f86-897fd6418464": { + "name": "Feitian ePass FIDO2-NFC Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4": { + "name": "Safenet eToken FIDO", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "4b3f8944-d4f2-4d21-bb19-764a986ec160": { + "name": "KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "5343502d-5343-5343-6172-644649444f32": { + "name": "ESS Smart Card Inc. Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "09591fc6-9811-48f7-8f57-b9f23df6413f": { + "name": "Pone Biometrics OFFPAD Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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" + }, + "7e3f3d30-3557-4442-bdae-139312178b39": { + "name": "RSA DS100", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "73bb0cd4-e502-49b8-9c6f-b59445bf720b": { + "name": "YubiKey 5 FIPS Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAfCAYAAACGVs+MAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAHYYAAB2GAV2iE4EAAAbNSURBVFhHpVd7TNV1FD/3d59weQSIgS9AQAXcFLAQZi9fpeVz1tY/WTZr5Wxpc7W5knLa5jI3Z85srS2nM2sjtWwZS7IUH4H4xCnEQx4DAZF74V7us885v9/lInBvVJ/B4Pv9nu/5nu/5nvM556fzA/Qv0Hb/IrX3VFKPo45cnm4inUIWYwLFRmZQUuwjFG/N1iRHh1EZ0NRVRudqt1Bd+2nSKyS/Ohys0+lk3e/3kQ9qvD4ZUta4VVSUuY0eipyiThAfocoORVgDuuw3qKRiAd3rbcEtjTjYIof6WaHsCmzVPWCMx+cgh8tLqWMKaMWsUjLqo2RtJIQ0oOzmerpQu4esZgsONkGxH7d0kdvTT17s4OMU7VI8ZhjgGaM+Aq9iENu8Pif1udz07MwvKWf8GlVoCEY04PC5WdTaXYFbR8vNvL5+3Kgfb5xNMya9RamJiynaMlGTVtFlr6ba9u+pqnEX4uMuRRgjSYEhrN7utFFe6lqal7Nfkw5imAGHynPpbk8VmY0xstnptlFCVCYtzTuBN83QpMLjTtevdPzSUnJ7e8mkjxZ39fXbKDfldZqbvU+TUgGnBVF6fQ2iPHg4W16UWUwvzbk16sMZE+Pn0pvz7JSeuAyes8lcpCmaKuo/p+qWr2UcwIAHWrvP0YEzhXAtLAbssHhp7iGamvyijP8ryqrXUWX9XoowxyAufNBrp43POBFXZlkf8MDRiqcpyowAwpuz2x+fWvz/Dtde9smszygtcR6C1wbdzBl6Olq5WNYY4oGathJMrkTEx0jARSHAVs+5rYkQNXb+QgfPLsQ6gXyInsreQfmpm7RVFYfL86n1fiUOkYvShkUPxvbukzoy6K1ihM1ho3XzW6EvSfXA+dpiWGaWd+doXzLzmGwKYFLCAsRAlPBAhMlCFXU7tBUVPr8HgVcJHWq+F00plr+DMTdrP4zvxY11kNMhxT+SeTGg+d4V5LQJityUGJNB8VFZsjgYBZM/II/XCTkj0qyDOpF2AVQ17CIjUp/DnT1UkL5F5gdj+sS1wg1gE3gigm60fCXzSnPXbyAPbIXv+IDpE16ThaHIS9skyhlmME5F3cfqAKhq2C0E5PH1gYaXaLPDkZG0HDJOnKWHp51I0z5SOux8e1WAuZzdHQrTkp8TmjXoI+la0wGZszubqbO3ifQ6A/W7vVSYsV3mR0JKwkKc4WHiBkmR8I3CCgI87oOL4qzT5P+RUJBejEOgAPK8hYPzatM+eITp2IO9yTQmeromPRxx1qxAcsile/ubSeEbcWQGYECghcLY2HyKjogjH25hMpjpUv1Ougli4eh2eRw0O32bJjkyuCgNzg0vzlYMSiSs0uoo4MG7hMOjCEaX1yFE0nSvjBzuTnEpK86Z8IoqFAIubw8kg9ArEaREWSZI+jH4Xbp6g9E9EnJT3oaRzDN+MUJBQDHn56a8oUmEBusOxBs/N5+tJEbPkAFDj8UGvOs/IWvcSglGBhvS7/FTYfpWGYdDY8fPAxWSA35sTC4p4+Lm4AaqIoPeQtfufK6Jh0ZhxlbsUXOSmXNifD5ZTAkyDofbbcclxnA8WNAqxCbRNykhXxQpaDw67fXUYbsiG0Khtv2oeIvh8rhQMYOcEAqXG/eI+zngOc5yxr8q82IAM1c/FLFOplqu5eFQXrMZzGcVCjYbLWG5I4BT1euRrlbxtNOtMitDDEhLXIIynAAvuOEWE3X3NdAft94VgaG42XIQt0ZX6PeCE/qQFe9rK6Hx7YU50KvH7fW4fS+q7KKBJxsggBX5pSAGh1jIrVh5zQ6w3RfaahBXm/aCbCZTjCUFUTyWZqW9p62MjJPXVqOrPgMO4Nv74Gkf+owftNVBDQnjFJqHSw17pXvhWW5KZqe/Q49N/USTCAVWoQXFIHBHXXe3FPrUDsuGDmtF/hHKTHpekxhiAOPI+SJq6S6HF4I9YWzkBJTo46iUMzWp8Pir/RiduLxKYsSksV8vLlOQvhGX2YlR0OBhBjC+u/gEcvY0ApK7Yk41NxjPSQnWFHTF66UrjgevB8Cu5a+l2vYSRPtuVDo73hhdMSHnUX7tTjsVZGxAl/WptiOIEQ1gnL29mX6/tR1tmlkYj8W4X+CSjWcUDGY1NpS/C7hSKqiMLM/l2QmSWZ73Ddz+gio8BCENYPQ46qnkzwXUbqvBkxjUQsWfZFgbuo3rAf+wN7jOO90+ynx4Pi3L+0nYL1SchDUgAP4gPV/7Id1q+1HShmuGkIqWRPgyxMFqP8HfjTnjXwY5bQfbJct6OIzKgMHotF/He1egsaxHSqG6wfdmQ5x8NyTFFqBcp2iSowHR3yk5+36hF7vXAAAAAElFTkSuQmCC" + }, + "149a2021-8ef6-4133-96b8-81f8d5b7f1f5": { + "name": "Security Key by Yubico with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "175cd298-83d2-4a26-b637-313c07a6434e": { + "name": "Chunghwa Telecom FIDO2 Smart Card Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "3b1adb99-0dfe-46fd-90b8-7f7614a4de2a": { + "name": "GoTrust Idem Key FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "998f358b-2dd2-4cbe-a43a-e8107438dfb3": { + "name": "OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "61250591-b2bc-4456-b719-0b17be90bb30": { + "name": "eWBM eFPA FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "f8a011f3-8c0a-4d15-8006-17111f9edc7d": { + "name": "Security Key by Yubico", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "8976631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "516d3969-5a57-5651-5958-4e7a49434167": { + "name": "SmartDisplayer BobeePass FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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MIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhOEV86zZ/8PMp0hD/Ud//AAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,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MIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhOEV86zZ/8PMp0hD/Ud//AAAAAASUVORK5CYII=" + }, + "2c0df832-92de-4be1-8412-88a8f074df4a": { + "name": "Feitian FIDO Smart Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "c5703116-972b-4851-a3e7-ae1259843399": { + "name": "NEOWAVE Badgeo FIDO2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC" + }, + "c80dbd9a-533f-4a17-b941-1a2f1c7cedff": { + "name": "HID Crescendo C3000", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "820d89ed-d65a-409e-85cb-f73f0578f82a": { + "name": "IDmelon iOS Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=" + }, + "b6ede29c-3772-412c-8a78-539c1f4c62d2": { + "name": "Feitian BioPass FIDO2 Plus Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "85203421-48f9-4355-9bc8-8a53846e5083": { + "name": "YubiKey 5 FIPS Series with Lightning", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d821a7d4-e97c-4cb6-bd82-4237731fd4be": { + "name": "Hyper FIDO Bio Security Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "9876631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "Somu Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "f4c63eff-d26c-4248-801c-3736c7eaa93a": { + "name": "FIDO KeyPass S3", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhYAAADfBAMAAABYEYe1AAAAG1BMVEUATJhAebKApsy/0uXeu1zmzIXv3a737tb////LZn6SAAAPyElEQVR42u2dTZKjOhLHAW+8VFRtWFL0hiX2bDhAdb8L9GIO8GJmDjAx8ZZIet3NsccIBPpISSkM1eCyF13RGAvpRyr/qQ+SJNE+py78+fWuf76BZ9HE/Khl8+J2IGu6XX3MCiOq9wPForZY5AoKIo6kza5ZIAzjO4oFsVgoX96soizfcGb4+1ik0WYBs+AWikztP8IimGYr+2MRrt2fKBatVfBZtZlqZJLtmkUa6TkdLIrE7YnY1DfqpNkzi5Bh/BfHwsf4RoCXt091o3LeNYss0ixAFl5FJVnvKgQeku2ahd9sf+BYeBX1ZgxM6Eh1O2/fLE4xgupg4VNUlvSdoz/pfLOfZtcsfNUDzAJiwXw9r+cgWBRnwWXXLE4xnhNk0fq6SDuy4P3BvbNIYjwnyMKnqF2bScNphO/YN4s8xiwAFtwbtUwsXrsDsHDGW+84Fv4x6sCiLISt7J6FyzB+IFnYilp1tr8QgHbvL1yG8R3JwjtGvVnNwKJO+960exawrP58x7Fg/lhWxhfD32b3LE5ozwmwaP19rtdSEV/crtLuPO50GcavdyQLEiiNnAYWt05SZAdgcUKbhcWCh9yPGKf2naXkOx+nOg3jO5IF9U3jDCfkE5XqCCxynKACLPyKOnSiZrSgtDsCixRrFhaLcExfD8LCyc7nO12G8fMdyYIhfA9J0qq7kn2ZhYdFivOcFosWEcUy/GrMLljovfzXO5YFwejzMMdXdUdhoZn231gWHBnQl2XZdYdhod3P71gWQUXd7yfBGcaPdyyLIqioh2ShGMafaBZJ/KrkIVjkCBUxWLC4afXjsEjj+0gdv0B7DBZKO7C+k8QtPR2IBcowvnkVNesehIViGDgWB1bUIItTZAx+YEUNsph7+y8UiwMrapgFwjC++brI6YFYzIbxE8HiyIqKYHGOmeMj+KXZ/bMgC2T122MoqsXiy4J465tnGud8YBZV7TGM4FpRceCgE2DBPYYxyurXP10sDq2oNgvA/Zmy+k8Xi2MrKsCChuKtr52LxbEVFWDhNYzee/7hZHFsRYVY+Ayjl9W/XCzYsRUVYsETr2H80blYtAfvIgCLzierf7//x8ni4IoKsvDK6tfOxYIfXFFBFl7D6Jwsjq6oMAsaWGiGWdRHnsZxsgDEMQuzOLqiOlhQ/3oiyOLwiupg4Y23HCwOr6guFrXXMEAW5OiK6mLhjbdAFsdXVBcLv6xCLOixp3F8LHzxFsji+IrqZNH5Jj4hFsnhFdXNwhdvASzY4YNODwvAMM4eFvXxFdXDgrodAMDiARTVw4LH+ItHUFQPC8vsfTryEO7Cw4JHxBePbheGYfjjzuKh/YXpPf3jkfahdcQQh8A49QFG7H4W1PEFFHeSB447DcNIQ/NajzweMbxncL7zvplf1qeMKRv/CXrdrv0vvixvuMhSo5fpZcHx8+D3qCqT9mckYuOSkMjSpk0CXOVVyjBUiBqTwqc+w+I37Bq8x+C81nJVfXNdn42HxixtMwuuXEzn1wKtKGxXflUu+YpkwcFmrTvfWTgrwPRtEBMLrjlqLZEdjsUFVrzg05NA1191HvzirsHIgprjZEOzVBgoFqZve8WxoNDRNddHLJ+rtGxkQQwWtQc6hgV3SV5ICAngBbHrZohO4p1kHli8GfMnwCzTaxQL55RdiAUF2rTiemrte/6RgXNJxLchF8GCOnc3h1hwQBzXW2fn3gc1QBa1d6c2ggVx7m4OBou1fXvX239Rex+MhVhw/07tMAvq3vYeZMHtFqH35ZyXmMV8lyEWrf8nYRYkcabRCg8i/tEhWcTvgJ4alt3CQvZm3mWVRWk25aWCfhJkMVeyj05Lovr5JQOq1fbxEV0HroYAMVtoqVFRZvwkyKI1SiyU27Ymi1hV5aYkUt2emO14apfWZEgWxIxiivkSa7KIVdXW6km1Zk/MRmoFFNNFGxQLbt8kMh1Zk0XsfvDCVhuiVoNZpTCgWKK2LsSC2o6MTbdtVRaRqgrYDlUFiFly1AJdj6rFhFjUQLVqyXdVFnGqyiBc6n2zM4YUkLUR5WCIBQEKoPIqq7KIU9UWzjc1Owy7RzjSMs3EAiw46NLJeHBVFnGqWocaxuDBmtnxuFL1AAu4AFmRdVlEqSoBr6jUg5lFUNghF2ZY6mTRgsbKxqPrsohSVdhwlIYxs04tXKRyOMCihp3YiHhdFjGqymGH0losTE5557ivYRYF3Mjh8NosIlSVwTdZaZiDRQVTTREsCFyl8Vcrs4hQVQp/zU0WWahXqccDLBwF0E1YRKhq67jgAhZFFIvMYaL5yiwiNiu5WJCpYVYvcsGd+46fBfOyOK/NAq+qrYNUYbA4Q67kHhZnRydbnQVeVeugwaNZtPtkgVfVYm8sTquzQGeJWcrifC+LysEiW50FepO8i0X7OCzQqrqURXYcFmhV/QwssKpaR7Pg62jqB7LA7gFuo1l0W7HgG2kqWlVdsVaIRdJ5QtVlLNhmLJCq6orBF7Dwj82SIAu6GQukqlKHL/GwWDZmD7Notxmb4VXVMX8RZnGGqSqtzSNZFJuM2SNU1aUKHhaOOb56PgyEY8ycKHMNjZstWCBV1dH7PSwcc7/EbO3JMWfEfH1s9Tm+GFV1zF56WDDfxsvKcB36nck9LOgm8+BRqlr7ZrVhgwfJtqqBuZZoKw+LYix1ExY4VaWwL/GxKKB+pa0LEtPYuLX4VMFdJN+GBU5VOdx/fCyCa8v2imtrLUpWsHtrtmGB3NJHbMPguZdFcM+BbHpjfJ1pLLRt0zzZZJ09UlVrK/rg5Oxl0dm++KIv3DKjTM1qJAtNyYtt9l9EqiozQzFOkgCL2iyOOTa457qF5joLBcYl2WZfTqyqEmN/FklCLKaCx+dBrhbrQm3J9ERBY7CYnht5UzYEbMMCmfZgNp/+PdRvQ8u9LObel5U3EoVd/LRy99J0ZQJthquG2r186fi8p/G0HQvc42fAXtcQC+rf6+rYP5sDLOzqbcQCOVato1k49u02/hOaEIu0244FUlV5PAvqNwvwBH1TdeV6Kf1GLLAzwHU0C+i+a/GC59JOFmm3JQvk42ecRLNgSaBo6rQaJ4tmUxbYGWAWzcK2pVNgONSEWLx2m7JAJxO6RLMw25oGvFDemSwIPO22GQucqpowsg7BQoeRNn5je+0sFnrPxD6TuZwFPvnYVa83goX6eG8W6Hmvnc1CozkXkJTap8Gw+N+/tM+/wZN4aX6cD5/Lx5Czarrp6VwEeHvkY9/pF3+R+msGlTH79Ny4UsBOUjMojS4wTySNoKq477X5C27dn/2lqUCyWPJhfj/wZLFnFqgnGJ8sPhsLhn0U/hOwoPg45+FZ1Oh0EY/PguCyAnwGFjTZTlKPxqLeUEYOxoIlG7rOg7EgyYauc+8sSmhiIvucLLRRd4HMIfKYLLiS5+NK0LllHpKFnIAqy9BU1eOzoJ4Vv8/GovXllPpkLOqPNIudsyg+0ix2zsK/ZP7ZWeTdJ2Vh7zl47T4rCy1b7eYodj82YwqNtNqYvH9tcAfjVF4WcjPZb3Ze3fPzZPFk8WTxZPFk8WTxZPFk8WTxZPFk8WTxZPFk8WTxZPHgLK5lGdiP7fmw8q73tOmzdDGb+jdhcblrc0DrfPADV2X17b8kWW8nzyIWNFmDxdJlIbXhdfK7WZCVWIzLhRzoLqzCsODJIhbXZjUWPFmJxbgcQgADIQmGRbuIBXM8B5gs6yJZrFmfdRZyXcQy+hl3JdZ2/CyKRatLrTDIwmpEsuy+NnezGBxwfh8LsmhVfk0WdfSmQ5BFf1ez4a+jjwRZLHv3+dBH1mFRRO+oglnwUUo40Nl5g2OxyGOxbkUW6SosegPL/TdwGxZdt2YfafQwshpCUTWwnM7gt+MOFtSwcWZEoyqLOVLVWGRmOPwlFPNWq7JotSawQdKKOV64zA9+3v6QKbvb2WTB5XvFhvq9aRmPKvXlCkN8mdm+U3O7+mN7fXFnNr2kJeuLo+NT4vJkkW1NnrBQU2cpG2qZ0vkpe6qlGiGJm0Xflrlxl0TLMaixINprCyvFQpVIvrYzbZksyNosuLprhFqbrYgSVWpZ/2wWtcKCq2UYLGr1O4UFVYc1WgEyJaHBIlmbxVC1VwW/utuKqv8JsGjH981V6tDiZLNI9BQ/lTYakDC0AmTyxs1ZcGWXmagLEWwuohqFONI/Lz6m76qEj1NG+BqLSjbu9oOXoZOrLPrxuMBbDbU3WNB5wMuH+3MZDIOKVEtlZbLIpuvf1FD44ztZKA8LMnG76dAjSH8pGTTItG65W1P730kW8qW1RFJQ4s56tEKioFPdpYAhRwZEXLKYU7lqLBqFheaKF7MY/Vwjr8WTKZRj0nnVo3E3HhZsbhwdDU14eoMFGf8O3+naMQlMPRynUz65BmBx6tZnMcA4iWvNje0vSGVVqZLtzcVC6SP1nIEzM1lM2gCwGHQslxHgqPBMScGnsjhvwWKAUcnqzizU4KHqJhkMsphCn8JiwaefF8Mt12J2ASMdgbHZgVcQi2oTFgLGCWKRKrHy2AoPi25icZJykBos2NT6YnBI+vhFwMjFNTiZk6s0H8hCCAbEIsOzKFQWZ3V2I4KFINC/we88oZgvYbDoNmLRX/lOFmQMyCrlFJBF52MxSMitAKK+7O9DWQjPfRcLdTxCltvFIObDS3Zu39O0/HgWFGaB9xftcPzePjJoiLCLdCzgw1kwmEWCZkHkAE7VEZtFN01z+FmcahF09ers0hGQxXkru5hvYpAFTeSPq1E+nCxOIRZZz4M10glN8QUfORYhFsV9LHrhsliMIWjHqyALog7CW9lEgEUx/r36/EU9p44/jcMkhWPiZEHkWG59HelbKCw1yKKQyxqy3aJuV51FM/6gV0qeuXXkNI1HanGxQhrGEL+L6F1nUc/vEW2GeG4Ji3FhmSisFRbD/Mo1UVkUYsCpjFO5+PmUN3Mep14SnUU/vhXRZJ9Fz2IxVERMYMlx6lUfp/bVycRtqwwWt39f+nFqLUfX+bK533ndzGYxTWspLGpz/kLL7aDPXygspldRjNNDFgttEqlWlya5rCCd00TqLGTWkemE5j4WDcBiet+8woLCLF7VyVwOsJDv/aIhFqk6r5XPI+mzml5dZzG9/0Sm9uvuYjGXr7JgNovheiaLTJ/kv9gsqD7feXKyqFzzneorHHQWw+mzIed3sXjpQBZDo15VTR34GCy+mAseotyXdh6bjUVNg9HGxaJyzoPLo1VnseAyZC9GG91kjxJbuOvm2ju7FlyV46i9N/D6yDW0gehtuDH726/VbpfzIbA+t8c8jU8Wa6yRPhgLuu0j7QdhcR0XzLbMdHAUFkUfE2+ZLudALMgHZH04Covt86IcjkXzZNGHlr2zaH7LlcWWov8DwifEzKp4rUgAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,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" + }, + "d384db22-4d50-ebde-2eac-5765cf1e2a44": { + "name": "Excelsecu eSecu FIDO2 Fingerprint Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3LWFiODctOTFjM2IzZjBiNzhlIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZWMxZTg3MjEtNzM3YS0wNTRlLWEzYTktNTFkMTMzNDZlZTI5IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjE4NWYyYmYtODVmOS1jZjQ3LWFiODctOTFjM2IzZjBiNzhlIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyMTg1ZjJiZi04NWY5LWNmNDctYWI4Ny05MWMzYjNmMGI3OGUiIHN0RXZ0OndoZW49IjIwMTgtMDUtMjNUMTQ6NDA6NTUrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/0VxRQAAGfVJREFUaAXVwXfcn3V97/HX5/v9Xtdv3Ds7JJAIAULYBZmCimDVDlftw23HqYuqPV0WtdbWR63nVG2rnraOtshDrRUfPR3WWS3KVhAZYQoEQkLWndzzN67r+n7e504iKNWO858+n2nuisS/J3G8YZeZ2ZTEImD85+ROO0ZSUfiHJP6FHyIEWBjAwzNw6obI3CykCGaGJNyhLMWwgnropNJICBNUcooi0O8b+xfF6PLAqIMcGod2W+zYD9Fg49rAgb1i0TJTHWGCuo6UheEJdi9mVrSN8cKYq42d+8SKCSO2gAwdIBQQTPx7ZlDVdkkWbzTZcKTI3dhvvrGlueM9d8UTX0Rr+jmoyYCQOMSsBLpAAjLQRxpgxo+RAmlr4ocIZheGkF5lBpL4rwhICXLDfH+gDxeFkHgCCeSwf78hEz/KjMPED5IgRXuRuf20pYBZQ72f7StGH3YmTvxFMhcgAwliARLgGWwGNAfWQqwmhshBcn4sGOA+l8qCxxmQBU3DSZIj8V8TYFC0jYUFbe31dP2y5ZAzTxAS5MZAgPGjzQBB1YDxA9ZZ0KkmcEHImc93Lvi3HfHIkqZejTIgMEAO7l8nxk8h3YLn3YQ0jusM1LyOEM5E4seCgOz/lPYcEI9xQTtxxHg3nukYIL5rEdgOCCj4fgYSsR5qRaejq0Jiuqp4ghQNLw1V4seFAK9FMr5HQLTjQgybMciNg7Hn1pWXfOOh6sSL8PkjMQdLYGGawd7fJXYvR0WfEMAC1BWE4lZ6C/9Mmf6OcuTpSID4kWUG0m7Evem2bc5jho1YOxmPOnMTp2aJ7ICBiY8J/T7QAkYAcZAAQ8Eoc0O2yLbRUUMCM5CMdhv2zTlkI/JjRGARQhHIjXiMGcdKGneM0jKIOx6pV+/LZucj7xAMSPvo6xV49QXSOMzNw8gEdFowMwMjY5DSXprmrRT6B4xViB9dEktuJNqOtHc+8Jj+EDpd2xTajGgAGeMgd/9nYE8I4IIQQCwJgIMLXBANmgySkR2K4Nz9IDw6LzYfLQrjx4YZNDX0ek53LCBxSAp2jplhghY1szZx01XNBXMEthAqQBW95h006QvEEahJtMuXUMQX0FRX02p9hCLNowCersf8PrBV/KfEYcZ/nzjM+AHuEAL/ITlgYMZhBq6bEQvpSUdGHlPVxBVjdo6y4RIgENsEO6JBlpECVLUTghFLQTYcIyMKQZMhG1QNFKX45j1iYtJoJUOV+CEMGAECMA+I/w8CXGCAO1jkv81YIsgOEoeIwyxAXYm5/c6qlYZnaDJH5czJhIBMmOAh3/jlgXVWQz6RYDAYXstC/Rd0lkM5AvI3UHTfRwBqfx4jo1uBL2IR6gDZG0IABO4QI2DgDiYOsQRykIMZP0jgGULicRYAgQvMOEQCMyha4BnkPIEEFqBoQa7AHUIEBDnficjppElxiIDIms6YnZkbaDJYMDz73cgfmWkCRYLJCP0+WAAKHmeAZEgQAgTjkNE2pAgShwjIAozjgZ9BOk+wzsBc7AO+gvikxKP8JwS4GDG4KEXOEqzqtPAA3zHjC4Kt/BcEy4Jx8WibM2JkKooaeAD4CuLbGBQlxBEjZkGf9XVtm4hgCIzZv+XFDz0YNp6NLaxEDmXns0yZEyoo0xnI/oicoakhRMBeg3wTUkn21RgnE8QhrQ4og2cHbQf24qwi2HqSBRqBADMe5w6pgM4YDHqQGzCDkCAVMOyBHCwAAgGxADl4BoscZqAMCGILwjhUPaFswA6C7mFJmnlUHOQZWl1Wj4yyRUEgkBtlyT2tqAN754W5sWRCcKrgDLDjgOUGCoGdGLcC/yp4hB9GEOCYqXZ4bW7sRdF0FGaGIAMpQsCeZYFfM7N3CP7aQHwfATmrRPZLrcivYGyWWVeCtZMgl5rK3pSiPobzh8CA7yMgi1GZXepur4zGpg2rYlnXAjeUhDsPWeTPLfLH1UDafm+mLoyRtv3EZNcmqyxaNCBuvT6euwPxMtRv4+rRG9xIMug0MNQBLNxPa2QLuYFqAMTnA8/noCIAxiEhgucDLPY+TjP4EuNj9+DWJ4RANXM6dN/CyLKzWJwFbyBEQBBLUIDFmQdxXUcq7sTCgGH/KPpzz6AzehIGNA2kNnjewfbbPsrY6vtoTz4fa16IBcgZWiOQ60fYfv+HmFhxB93Rn8Pzy3DdjrGdJam7MXCQBEXkDDPGcgUWwXAGfV1fW0Buay3y87g9v922Ew1bITcwgSAFQ8Jj4H6ZXVFLHwBm+S4HArx49TJ7R9kKxw8WwQKPk6BsQQGWzdYXo/GjdZOjMh82DpMgJjtp9UT8391kF+eGokjCJbIMlxBYrnVku2tvMw9HmvJrBQOWOFAETlnVDh9sWbigccNM1BnEkiAkkLEhBHt3GWwVmd+8d5vzxe/E9Myz7cyLz4fqESiV2Vls+PyeYm2PPk/FMsgHDPozWICqgm7nATy/gNk9r6Eon0d79Ek0FYcICAHEEoEPv8qjD7yTVcddw8R4QzWALBBg+WFmFr/KbHMFU+XzCAmygwUo0x72PfSXPHDn37LlKQ9h1idEwGFm1yo6x7yVsvtG6hkwoDP6NhZmLmfZxhYpXYzXIAGCaCC9i179FzTXQTrhQspN4IvfAuZZkrpdcZCgE2VnezZcImK0Onx1dtb+Lje6eNUK+2DCjq9dhBC05ADSiAXKVjSaRjQixGDHgr3T4FnAr0p82wWdyFtbI+G3TTbeuBAQgBAN5PMjLT53x4O6etsC+84/wdZOYi9tiO8yy7ci3chB4txWyz4S4cQiQOg6vR57TFyVgjyYXSRY1QAOdGJ8qaRrJPtoU3PQuSnYFaPRNmWDjDDYWdV+vRnZ4Gwz22BANZSVnfiqo47ls5POVfPLbO2KUdtMX2AGBQw6E9c0d+1dxdrjNtFOoDhCZ/957HhgK0efC6EG5x4Gi79OSh8gpKcR/dcou6fQn4fskCJQ/z3Ub2BqzU6aPowsO5bh4AJcu/Dmq7QnBvSZZ/vWtzN27Gl0JzcyWATZ9VRzb6bdvobN54qiBWqgGoIitEf3sOfAmxi3SLd9KVV/F63uVzj6LIjFOlRdgAUQEAMMq3vJdhVr1kJuLcMmn4oqoL4ZPIORGHCIGVNEThJgBtn9y8MBrx8ds7cFhXd2ohg2fmPO+nSQ3Qy2D9NkU9kpi42/oGyFi8pIkAtvxMSYnR+K+AkLzYtG23ZBuwxvyz2160aYQZFAUPV7/qmisD9nVLf1+vSne44sQNYVjeztpfHURn4TsM4svM/EiSHBTF/9hUX707Ktj4602IXIN9zVbJ4ai+/fcnS4sBqIxlW0Y3zdvgU+um3ajzjtKP4MbFMtkGnOs783hPDJEOxRSRgciXgbxksFlqKtaKf4wv5QV516rJ60yjmh2m9YEJTsfo9e/8h9BzaewRHzU4QCFFqE8Aa8uomiuIWmD56hLMDig7RHHuSWa7/EsP9RTnn6s4gGi/W1yN5IHOykM7GMhYU3s7j4UsRqilAgPk6Ov0673stR628nhxvI2kh3/CbmF1+LuI3xNeDh6VT9VyGORPlmGv9TJlbtxID54V/Saj8XfCdzexexNtTVWUTfgBmYQTDoDXfQ0zYmWpA2noP7CfhgHyHfjomDkjjMxPpAOA4Dz9wg8X7V+r2RTnz5Yq0Hds/lPxwp7TPBmOO7gkHlXHv3w/6xiSn/+VM2pbdXs/Ykj2I4EKEKW556UvHlmJioemorc0grQQOPHhj6W2nsb8qCx8UIMRi49tdZf1AUXDBWpomFSr9lFs4JCAvM7Zr1S/vzfHzDesMMEDRut873mrcop/cEWB8DzXRP93/qOi/OPzn9amvUnrwwC5ge8tpfBXyNJ7ob9DuYnWjYaZ7FYrZNMcNK2JKCjVdmdBnAgBsf0hHb2LLudaQDI1QVyKCz6mSOmfok7n+M/Et4/QitUeiOgzcg7WDY+z1yPomiXE9jf4hpB6b1pHg54yufwXAAZhANXC+nam4l8B6649BKB8gLMNd7J5Vuo4qREbuMwcJvY2EMi1CMXoSqDthlxAAdzdI0eyk732I4nOOuu2H96tNZtTwxrCAYxAQL+2/CrM/oauhVT6ZVdJhurqetA3QiOKQUje86xYwpwU7Hr20ne0v2dG4/6+vu/ipgG99lgFhiHNI4vUa6HPdv7hvwibFOODUBuRHjIxyRHeoGgkEMsGtG387B31h27GoJEODQbUO3Mu7dnlnZEWXBVLsdO5Y5Xh5eoCiKCDNz+UPT+/zjrZSQwIA6w9pJZzD0awfz+eeSaSwmcpXZNTVqp69ZYb8iB8+OR96dUvxaMEYlGWBLWJKBA3J924zTWOKoXDSnK9uYJAQEgwPN6NW7e2ugzdmQQSwR4NDubMb9r8jFVqI+AfYZot+H+nD0aSz5Bsq30BvsgvANmj3gfhRh+TShuRJ5BYiGAhgh6B6KBAasWH46X7/yc1jrK+x7ADY+8+XE+AcIwwRiSYZ2+UtIZ1A3MxRhAmkzln6fbdsaRIeiOJWDDJBDw4D22LcY9mB2DkJ6MrRgqnMzTX2AbByUkFjSwux0CQyfjm7PDeNh06DUF1p9vZzGpuWAQAYZMMAM3CEA3TZQsHWu1s/UMf/VUd1wSb+GQQ0GmEGIQApff3R/fu3KFdzlAjNQgGYIJ22AZpv40OfhwjMDzz3dLt25x+Ro4+rltiwPIXS4p13yJ1PzRrsFqQV1AwZ0S2M4BEk7DJFlrBiNxYvP54VkVizOiZBsEemngLME44D4nhooDM7iIAODxWgU0ThJAtwgwZfjJXdsDSe2CPkIVAMBMBDQDDkkdU7Euu+iHrwaeAmTozfgwGIFqIf4BKVP0x9C5jq8uY5Q8D3GIcpQlNCdWMnevcv49rc+yrLOIivXrmCyuIzKDRNgPK7JXeBczMAdsPsxu42NR4H78ZThFOoKMEDg7GB0fCsR2Lv/BI5YtxkL8J0br6O3PxMLDkpkDpqk0OkgYrCjrWMj9+3RTdMLevU4TK8eg7IFbpANhAhBWANmcMRyY6SA/oLYvMy31zle2Wu4hCXGYWZQNf73/YpLy5Z2lQFKjNACBehV0CmEAAdiyXndbnrp1unmj8pRzl7fsnbdwM55v3rdlvDoyRsMGjHYATPT0EqwcsKwEFEw3CCHQITV0eyiWuAGEUbKEH7aAQnMDAQOGGAsCYYAA5R9ayfY6Ql7umSU7RrmeHB7/aTbB1Pd55B7G3DLYLs5rA02AUTUgAtSsZHsL2bPgRtoHCxvAFtDsK0YMHlcC08ryL2E6hqL4qAQurgmiUXBsP8wvdYrqPbMsn7l1Zz6HFi25kJy3shgHkLgCQwQICAVsDB7Lb3eblathRBPYXbfCg6yCFZA/5E7Ge6+ndFTYM2G0xlrH0Nv5gBX/eO9PHw3dEY5KClw0LGBcCoYoJFOS+zcmT+9Y5e2r15hdDvG2nFjUIEBBphgUIt2aRy5yrh9u5jtiRPW8Ryv7HfdjIB4TDDDG3v4zl3DfWunjNFWoh2MJkLtEIEA9IYwVjK+6aj4f+gqnLZJN2XF1wzmhRVUDNnaTAMm6gXRzBmt0pA7VQ2rlhc0bmQXMQnPrOkNOc6CiIYHWBCqBMkMY4mExYAlo19l9Tms7WbT9dA/VrTt9BitW1XQsQyJ665ZPHUHzs9igxLxBoyrgQI4HvQBzKZwQVmA5Dy86yYqwfIWdOIFMHICsd0DQTVYhzVXgE1BmAVzzEaAI4EaYz/YDKk6FzpXcMHPPkznKCCtp9ofeZyAwCFyiAkCmeyR1LqdXPWY2QNmJ5DKhDtYgPbYkMXZ/4tFiCuAAz9BM4R+/0Y2n7OLdcdBKjkoyQBjM9A1RBbUiyyun7C7jl4LT1pjzC7AYAhmPEEwkKBqIDsEC78I9qc1jEeE+B530WmFX142mu6qc/6wAxlwAQYIqgxjHVa88qJwxUmrwmmPPly/eqodDySz5XUjYm3FiraWz+4WQSKZEVqgisMETaOOjGyoaHfFcNFGlBkLLDELg+x/Hcw/UgQ7KrsiQg4qZHm20e6W2ZxxSLdpvJ2d+wrs9TlDLA0GkUU1dzQTu6DiGJLNY3wWtA0MpPuBS8HOBYEE84t/QtH6OKuXQf9R8PZTaY+sYvb+BYYzMPKkfRTlPmI8HxzMQAb14MsEu5JQ3IL7y4iD80hjs7hVTO8B91tot2pSTMhABjSQ/XMU5VfBd7M42EIIl7Fm5RyjJXziz6CutvPcN2R6/UTTh8X9H6fV+RuqGaA/Tq5+gl4FqfUNLvz5/aQCJA5KJloW7GQzQxImY+j61oYjuNbN2DcLGJiBeJwBJTB0QQrW3bDC/qAswpuGtSXMOcjEfhkdoCPAXWPHLEvvne9jcj5iAee7hKhqe8bxa8L7WuviKffdnR/+5j360nOeTphMigxAYJV4aoxWFoTKlUEGBnII0X7ZjJcHVAmb2D/jfzbRsu8oWd+zuskgi/Yg+52jId6JGWYQgeyBPZXO3dANFwfRdTEm+TtapR8RzJ6R3eh0wfY3fGbfebddc+zLVlFrI4OqDWqDwAKgA8Bbwf8nKQVC61NUM59h1SS0OtAfvZii9QJMsLhtGckgNnNQ/jLKd0A8h5AXqPt/D91PEFOmGXYJcRliiTajZgr3abJdh/ROxG+hPEWIcyi8H5p3I1+kbqA//B3WroU7bzjAo/fD1BGw7bZPM6yOpCjOoan+lf7sB2lPQQR6u09gZORkHDD7JtUQqiGPSRaYDGZPFocZwkyr+xW/GQwrjEI8rhWMZYKVwOddfMhd58TC3rlqMpxfu2gaUQSjct0WsFcX0iuaaJfKRRa0IqNlN35g6P6zLn0O7CGDo8GeEYM9nRDG6LnPzuc3bZzioeZAXqbxsK1VhOXDSpjZBaXCR8z0Boc5lrizPJq9vSzt0ioTOy1jUGn20Wm/u73Btrfa3D+YtZOzYDTZa3pVmBs29rutksrMkBhPQb+4vh1+TzBlBlm6y4y3J2OF0BaLRr2YSSV3PbjqKV+bmVv3U8TekZgD8dm4303OEAOY/RuR62m1CtA81X4IU9BUmylb78fKZeQ+LH/yZRTDW6mb/eDTiLeT2qMMFobM7x6y+hTIfjTW/zgxnYsDFi6iGZ6C6d9opYzxxzS6imZwBGOj91OH2/DgZIdW+fsU6e20OrDnoROpdSWnPg3WbNpHtrexsDBCqzXHyCQ0DiHB/PRGxiZXYPVecvMQMr5fGhnV+oV5Oy1EDnFA2HGlwluiAcZhxiEu7TXZfULHhEKXE3ha5ayihmhGA9RZ/+TGb7jn78j9ESxeHCwcD2KYRTArkoXnuPjJAH2DtoKlgiUyWPRLJzv6h1gEFqfZ/8h2/c0Jx3NqUZJyA2Z6hdAWI/yrRLdT8EzHNsug0zKiaWeKegnGLQMpDOa5ciTYybULi2bdMv5GnXWhYVeDumZ2tsxOG41K2aGW3SDpJRY0INh5YAgDBwL3rIr7Fqk4DUtgBjG+mex3In0RM8iCfjNgcGDA7COQa5C9iFi8D1tYj9cgQWfiEurp9+LVH5HCvZg5+Bz9Piz0l7GOX4D8FhpbjsQhRiIW76YZ/gIp3oXUYM31pBLm52FQQXtqPa3wv5C/FDOYmYbTnv3bxPYOegsfYd2xMKwyg2qelj2bOh+L6y9ot0RafRG5BuVv4HoYxPdLuw9w3nhbHXcwQIIiQpFgWAl3sMAQ8Yjg9ib7rkQYiYU9H7N1LhEEjXDQ9YtDf380PtNqBc9AI+0I2X8ppXC5sGMdIQlxSBSMGlCYMWg0bda8voU+7dnwDJ0Iew7oY2saf9rqkfhzvVknm8zgzGDhTAEREYNRZdEfautYl1enxHWGyAfcLdtfxzF7Vtm28/p9sSSmZOe4cw4YBzlGPwt3/5cQwpswtg1rJmIRnhmCgaATKmY0ddvn9TwoOQvmOURaTQyXI/8Y8FVcDzB0GM6vYzg4hbXHP5MmP5O8WBITh5hBNQ90foGyfSGevwi2C29Ed/xIyvYFDBePBkpCAnGYZ7B4FmX7M8DloOsw7Samkrn+MXj9FLrpeeDH0TiYgWdojXao6/cSeDbD3q1kb2iXx+P2XFKMiJ8m2DixPA014NxMtlmMJ0jb9tnZZxxnDOfkBBQCw2GjhcVK02WyngVlyeYxTHBcCuECC4zWWVni3mS6rwjcOZe5vsq6Osr2SeIxBpi4buD5xQG7LJm90MFSMCRwiSLSm6n1jwuV3ruyxc0skURrMtDpGidMsZCC/aqyzwq9MkUrzI1GAoxa0E7a45Wu7A/1J2PdcD8CBKpEu9SOnMPL983z5xNtPSsRGGYoAkjgEgm/Z99QHy4jl3eD7R9UjmACOBWJQ8TiPlv+2ft13BbE6YQaCDXuhtkaiuLNoNeQwn5GCqNYPsmyI8aIRaLuQ64bQiEQhxlgEexoTK/joJyh1YGRSRjMC1ETAk+kQExbUH4XhBkIs7hKppYvw2wEr1nimDWAESIMemA2SozPR/58YoQEuACDYJcgB3OWOHAdQfx7afPq8MFqUZ/EaEAKwRZ7feYXKy0eudKyGpsaVkzGSNtgBOTIpptGM2ALKXEAmHfRuKBgifFEBln6lsP/kOuKYPaUoeuoEGwYpHvqxr9eK9zkMDS+TzSsMDoJAuz2rDcOh/nvKsVnWNDxLQiYpt11izJfk7TVzDKPMSAABiHw4N45veThPf6TW9bylLJgw6DCzNiZTNeY+HqWHhLG9EJN3YiU7MBIaa8RgSAlEotfqJ91813941fQ7b+SQMZVAYZkmLWRuhhtygQh1BiLVIsDjExIgPNEDQgDEpAIBrluyE2DmTCWiB+gJgAdjBHMEpKIcQj0aOohZg4YjzGWyJAiUCAHUQMNB0kRcEQbbBa4iR/i/wH3D5PMpd2t5QAAAABJRU5ErkJggg==" + }, + "b93fd961-f2e6-462f-b122-82002247de78": { + "name": "Android Authenticator with SafetyNet Attestation", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB7klEQVR4AaWPP2sUQRiHn5mdvexd/plEcvlDCi1E/EMabUWI9jaKWPoV/A7BQhAbG7t8CCUIKQQLuwhCUBsLBSUmGkLudm9n5nWHzMAego3P8Oy9s8vvfd+jzctPz2Ya+Zdbu48mG0ma8Eh8/bF3yWGGwPvV81d7+9/2lpy3Mrty7jswPPz8Yb20lQJ2iain2w9ok02aLURWstxuiHgknnrEK3GERg9poZ7s3CUxl/dvVfrntmRag9BuICJgrXfHnRvAWyJaDxXB+ezCWqX3t6e6i/ri/E1AkdBoLi/cZrL5pqeHb2yvu9RIUKfiWH95IVmmV6eucK1/j8JMIwRo6jNcX77P2vQ6ZEZ7OXreSFA93rnD3Mx6r7YfTxQKGkN4WP8eW7+bz4Z3eHEE9FFZAJXuliXVyUEfif9ZHINW+BQ5fSc+3oTjztTZRkx4LEhtfh1avBMSIkBrA+JvOAohm1AFgJGRpbOoXS/X1KXgHZE4X1Ssxpt18iYImGJiRFWWKCXkBdiR4L0QUEKamIKxhoQZm6fAdMDVjT7cQwBEYh3DSsl4A+trQTwJbUCsT5P+CodTZtYDmNJYcrEDQSChIMsVzoVQ2kLFMCCQFW4AoDbfbRDI7fIi5aAL41jtVNiQiPUjmUBOgAMCm683/ss/TaVXtx4qKMoAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB7klEQVR4AaWPP2sUQRiHn5mdvexd/plEcvlDCi1E/EMabUWI9jaKWPoV/A7BQhAbG7t8CCUIKQQLuwhCUBsLBSUmGkLudm9n5nWHzMAego3P8Oy9s8vvfd+jzctPz2Ya+Zdbu48mG0ma8Eh8/bF3yWGGwPvV81d7+9/2lpy3Mrty7jswPPz8Yb20lQJ2iain2w9ok02aLURWstxuiHgknnrEK3GERg9poZ7s3CUxl/dvVfrntmRag9BuICJgrXfHnRvAWyJaDxXB+ezCWqX3t6e6i/ri/E1AkdBoLi/cZrL5pqeHb2yvu9RIUKfiWH95IVmmV6eucK1/j8JMIwRo6jNcX77P2vQ6ZEZ7OXreSFA93rnD3Mx6r7YfTxQKGkN4WP8eW7+bz4Z3eHEE9FFZAJXuliXVyUEfif9ZHINW+BQ5fSc+3oTjztTZRkx4LEhtfh1avBMSIkBrA+JvOAohm1AFgJGRpbOoXS/X1KXgHZE4X1Ssxpt18iYImGJiRFWWKCXkBdiR4L0QUEKamIKxhoQZm6fAdMDVjT7cQwBEYh3DSsl4A+trQTwJbUCsT5P+CodTZtYDmNJYcrEDQSChIMsVzoVQ2kLFMCCQFW4AoDbfbRDI7fIi5aAL41jtVNiQiPUjmUBOgAMCm683/ss/TaVXtx4qKMoAAAAASUVORK5CYII=" + }, + "2fc0579f-8113-47ea-b116-bb5a8db9202a": { + "name": "YubiKey 5 Series with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d8522d9f-575b-4866-88a9-ba99fa02f35b": { + "name": "YubiKey Bio Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "50a45b0c-80e7-f944-bf29-f552bfa2e048": { + "name": "ACS FIDO Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "f7c558a0-f465-11e8-b568-0800200c9a66": { + "name": "KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "3f59672f-20aa-4afe-b6f4-7e5e916b6d98": { + "name": "Arculus FIDO 2.1 Key Card [P71]", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=" + }, + "42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3": { + "name": "Google Titan Security Key v2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAD1ElEQVR4AeyXU5gcTRSG62L927b/aJLpnt241zGmd2Pbxl1s29Yotm3bdqY3nN5oUakTq43leZ5v2Xi/qoMalBkiO84xTND1qNAwbyTdkshBtM4bZTtFvp97Tdu4SHo6UVu4Fu5Jc3AA4aKo0QTuNhFWKI5oPDzDdHACzcCKAog+gh2zFjIc/CYT+iN52TIibIQgxW4zlk8NSheq7PNtxwbrku5p5Y2gu8PDTdQDLspWUh/4KHqwqfCgCPqYl6G/NXLl0z/8jSiK1Qhz+7UZwJkKnxBj/dcbafMpBE56PsQqvq+TwN+eL4oDrjUMHoKLpFcphJ+s5OXXmLBfyQJ5DIGHdqlkml6PpiORyoCjB4E/pBs8Xof8+EHfnuCKWVNkwF+DVNP8TobxQ3pF8qoANmm1P37o/AgnxOcRgbf5rsdQOVF6i6TVAcvAAOjx0kB8p/HfQiYqpjd2SJ8vCXgSwL8uvucP2BtNvQ6/DKXHSF4dUBGA36cHkz7DCWUtAI+5aBuVPg2s8h8OsEJ6ND8EUuooSq9BILcBqLj82iKFEdGTx3oqvAe/TKiAr0kZeLzSn0pjA6BzQjuApUQK/cN0YCBJtQEEkfYGcJY1ACkUlJ4NcDKK2JKeDeySNLDav2E6MHBJYBL7jxeD51cFJfeel2+pCgOT5Qp6vOQc6MWvEjJQUwj+9IrPcAVPDKacLLbOYv9FBkVkT76t5A70ShwucJgL/vF98CuW/IyLuMoC/DM52OnGGfBtkzIQ2cNXUew4sekF+MPVAbjfgrwA/Y5oR1yk3vDhPRLLyqkBph//LYIQS6PrKz/CdWczACuka6Ez7D/qBc908X5I4I5J5n/PxE1SnwmCNi79/kasuyRASukY7Y7X5bNsRE/fPDmrT1KsKZIKymGvC4AydYpyls+paeV78Itkts/bTBcsb5ASsF0KTDygXPbOzORaiqZ0PhcbGzax65HwPl6Z/T+xs/yHO+1hBCwJAOXLztFOtjfcK/hcd/yflINtSq7b9uI+2/TGmBlwVPIILbD6wgEvgheo1G2ifUTrQAAMBoWup2dVwYWGLRcpXj4WqQmrk50MLzBLBcaOJIPq7psGevi6q29v6xg/yhXnMdOEbWo7HN733Iu895DU8QMWTSZg+pppgp5V7XHRwVtfwOsTJI9bQscxx0TcYFg4pHeQwWUhLzhkGDgUuotlkZEBK2N1sTVJgZ/TEf4BeWZ/y7xynyKzAgYX7bBXJEW+THpmCOqU1RHX0Tqr8pcoLQMOdmAGchd6/vPdSXonPWDCPxmwQADibHC/YhiAUQAA0S0KWSVGA04AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAD1ElEQVR4AeyXU5gcTRSG62L927b/aJLpnt241zGmd2Pbxl1s29Yotm3bdqY3nN5oUakTq43leZ5v2Xi/qoMalBkiO84xTND1qNAwbyTdkshBtM4bZTtFvp97Tdu4SHo6UVu4Fu5Jc3AA4aKo0QTuNhFWKI5oPDzDdHACzcCKAog+gh2zFjIc/CYT+iN52TIibIQgxW4zlk8NSheq7PNtxwbrku5p5Y2gu8PDTdQDLspWUh/4KHqwqfCgCPqYl6G/NXLl0z/8jSiK1Qhz+7UZwJkKnxBj/dcbafMpBE56PsQqvq+TwN+eL4oDrjUMHoKLpFcphJ+s5OXXmLBfyQJ5DIGHdqlkml6PpiORyoCjB4E/pBs8Xof8+EHfnuCKWVNkwF+DVNP8TobxQ3pF8qoANmm1P37o/AgnxOcRgbf5rsdQOVF6i6TVAcvAAOjx0kB8p/HfQiYqpjd2SJ8vCXgSwL8uvucP2BtNvQ6/DKXHSF4dUBGA36cHkz7DCWUtAI+5aBuVPg2s8h8OsEJ6ND8EUuooSq9BILcBqLj82iKFEdGTx3oqvAe/TKiAr0kZeLzSn0pjA6BzQjuApUQK/cN0YCBJtQEEkfYGcJY1ACkUlJ4NcDKK2JKeDeySNLDav2E6MHBJYBL7jxeD51cFJfeel2+pCgOT5Qp6vOQc6MWvEjJQUwj+9IrPcAVPDKacLLbOYv9FBkVkT76t5A70ShwucJgL/vF98CuW/IyLuMoC/DM52OnGGfBtkzIQ2cNXUew4sekF+MPVAbjfgrwA/Y5oR1yk3vDhPRLLyqkBph//LYIQS6PrKz/CdWczACuka6Ez7D/qBc908X5I4I5J5n/PxE1SnwmCNi79/kasuyRASukY7Y7X5bNsRE/fPDmrT1KsKZIKymGvC4AydYpyls+paeV78Itkts/bTBcsb5ASsF0KTDygXPbOzORaiqZ0PhcbGzax65HwPl6Z/T+xs/yHO+1hBCwJAOXLztFOtjfcK/hcd/yflINtSq7b9uI+2/TGmBlwVPIILbD6wgEvgheo1G2ifUTrQAAMBoWup2dVwYWGLRcpXj4WqQmrk50MLzBLBcaOJIPq7psGevi6q29v6xg/yhXnMdOEbWo7HN733Iu895DU8QMWTSZg+pppgp5V7XHRwVtfwOsTJI9bQscxx0TcYFg4pHeQwWUhLzhkGDgUuotlkZEBK2N1sTVJgZ/TEf4BeWZ/y7xynyKzAgYX7bBXJEW+THpmCOqU1RHX0Tqr8pcoLQMOdmAGchd6/vPdSXonPWDCPxmwQADibHC/YhiAUQAA0S0KWSVGA04AAAAASUVORK5CYII=" + }, + "361a3082-0278-4583-a16f-72a527f973e4": { + "name": "eWBM eFA500 FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAExCAYAAADvDYgqAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAFicSURBVHhe7d0HeBXF2sDxN73QCTVA6FIFFKkCUuyAEumKYkFUbICCIiKCUgQE7L0gdlQsKCpSrIggSC+hJnRCJ4H0b2fveD/0khCSnc2ek//vuXmYd46XkJNz9sy7M/NOQJZFAAAAAABAgQrUfwIAAAAAgAJEgg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeEBAlkW3PSszNVXSDyTKqa1b5dSadZK6e4+kHz9m94n3//mAcQEhoRJcupQER0VJWJVKEt6gvoRXryZBpUpJQCD34QAAAABf4NkEPSsjQ05t3iKHPvpEjv+wQNL37ZOs1DT9KICzCYyMlNAa1aTENZ2lZJfOElqhvPWOD9CPAgAAAPAazyXoKjE/Mvc7SXxzhpxasVL3AsiPgNAQKdqxvZS9Y4AUadJY9wIAAADwEk8l6Md/+132jHtKUtat1z0AnFa869VSYdgQCatSRfcAAAAA8AJPJOgZJ07InolT5PAHH4tkZupeAKYEhIdLhVEjJKpXdwkIDta9AAAAAApSgSfop7ZslR0DB0nq1u26B4ArAgKk2BWXSpXJEySoaFHdCQAAAKCgFGiCfuLP5RI/YJBkHDmiewC4LbxRQ6n25isSEhWlewAAAAAUhAJL0E8sXSY7brlDMpOSdA+AghJaq4bU/OhdCS5dWvcAAAAAcFuBHJCslrXH33kvyTngEambt8r2gYMkg/ckAAAAUGBcT9DTjx6T7bfdIRmHDuseAF5w8s+/ZOcjj0kWhRoBAACAAuHqEnd1xnn8Aw/JsS/m6J5zFxgZIUGlSklIjeoSVKK47gUKOettnL5vv6TtiJeMI0clKy1NP3DuKk4YK2X69NIRAAAAALe4mqAfXbjILgp3zkepBQZK+PkNJKp/PynWuqUElykjAUFB+kEAf8tMTZXUXbvk6Lfz5NDM9yV9z179SO4Fliwh5/3wDUXjAAAAAJe5lqBnJCVL3FXXSFrCTt2TO6E1q0vFUSOkeNs2dqIOIHcyT56Ugx98LPufeV4yjx3XvblToltXiZk6ybpCBOgeAAAAAKa5lvEemfP1uSXnVmJQomes1J4zW4pf0o7kHDhHgRERUvbW/lLLeg+po9TOxbFvv5dT8Qk6AgAAAOAGV7Jetex2//Mv6SgXrGQ8atBAiXlqvASGh+tOAHkRVqWyfYRa5MUtdc/ZZZ1Kkf3PvqAjAAAAAG5wJUE/sXiJpO/ao6OzCAiQ0jf3k+ih97O8FnCIutFV7dUXz2km/cSCRZJ+5KiOAAAAAJjmyh50Vbn96Gdf6ChnKoGo+fH7EhgWqnvyyfrxstLTJf3ECck4flyyUvNe3RpwizqtILhYMXuZul0Q0aGbVae275DNV14jWSkpuidnlZ6ZIqWv6aIjAAAAACYZT9BVcryueRvJPHxE9+QgOFiqz3pPijZprDvyLnndejk2f6Ek/bpYUrZslYzEg/oRwHcEx1SRiNq1pGiHdlKsY3sJq1hRP5J3+156VfZPmqqjnBW9rKNUf/VFHQEAAAAwyXiCnrxmrWzp2l1HOSva8RKp/vrLeZ4tzDyVIke+/U4SX3tTUtZt0L2AnwgMlKKd2kuZW/tLsRbN8/w+ST96VDZ1vFIyDh3WPdkLKl5c6v7xswSGhekeAAAAAKYY34Oe/NdK3Tq70n165S3pyMqS44t/l7jO3WTXkOEk5/BPmZlyYt4C2X7DLbJt4CBJyWOV9eASJaTEtV11lDN1VFvqzl06AgAAAGCS8QT95PrcJcsBkRFS7JK2Oso9VSF+98TJsuOmAZK6dZvuBfyYStR/WCibu14nh+d8Y8fnqkSXq3QrZ1lpaZLC+woAAABwhdkEPStL0rZu10HOwhvUl8DQcysMp4q+bb/jHjn46pv2XnegMMk8dlx2Dh4me6Y+Y7/XzkVEzRoSWLSojnKWsieXJzAAAAAAyBejCbra3p6RnKyjnKmzms+FSs633TpQkhb9pHuAQigjQxJfeMVeRXIuSXpARIQEl43SUc7Sd5OgAwAAAG4wO4OemSmZuTzOKahCed06O7XsNn7YCDm5bIXuAQo3tYrkwFszdHR26ui2gNDcFX7L2LdftwAAAACYZHwPugn7X3tTTnz3g44AKPsmTZOkFX/pCAAAAICvMXrMmtoXvqlLrKRujNM92YsaNFCihw3VUfZOboqTLV2us2fRcy0w0N5vG1wmSgKjSulOwKOsd2TGzl2SceKEZJ5I0p25E1qrhtT+8lMJjIjQPWeWlZEhcZ1jJWXjJt2TvZLdukqVaZN1BAAAAMAU30rQrX/qttvukBMLc7nv3D43uoOUHXCzhNerK8HFiukHAI/LzJS0w4flxO9/yIHnX7ISaes9lJu3akCAlB8xTMrdfqvuODMSdAAAAMB7fGqJe9Kq1blOzkNiqkj1j2ZK9VdfkKLNm5Gcw7cEBkpIVJSU6nyV1J4zWyo+OVoCwsP1gzmwkvjEl1+TjKRzm3kHAAAAUPB8J0G3Eo8Dr76hg5yFn99Aas7+SIpe1FT3AL5LFXQrc30f+4ZTUMkSujd7GYcOyxF1PjoAAAAAn+IzCXr6kaOS9MtvOspecMXyUu31lyWkdGndA/iHIo3Ol8rPTbVe5EG6J3tHPvtCtwAAAAD4Cp9J0JNWrpLMY8d1lI3AQKk4drSElCurOwD/UrzNxVLqhj46yl7ynysk4/gJHQEAAADwBb6ToP/2u25lL7xeHSnR4RIdAf6p7IBbJSA4WEfZyMiQpJUrdQAAAADAF/hMgp68bp1uZa9El6vt/bqAPwurXEkiWjXXUfZOrV6rWwAAAAB8gU8k6FkZmZK2abOOslfssk66Bfi3Ym3b6Fb2Uvfv1y0AAAAAvsBHEvR0O0k/m7AKFXQL8G+h1avpVvYyT3DUGgAAAOBLfGaJe64E6D8Bf8drHQAAAPA7/pWgAwAAAADgo0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPCMiy6LbjstLTZVOXWEndGKd7shc1aKBEDxuqo3/KTE2VDa3aS8ahQ7rnzBqsXS6BkZE6Mic1PkFOrd+gI/iz0JgYCa9XR0fecWT+AkkYMEhHZ1aiR6zETJ6go3/KysiQuM6xkrJxk+7JXsluXaXKtMk6AgAAAGAKCXoeHJz5vux+bKyO4M+i+veT6Mcf1ZF3kKADAAAA/ocl7gAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACABwRkWXTbcVnp6bKpS6ykbozTPdmLGjRQoocN1dE/ZaamyoZW7SXj0CHdc2YN1i6XwMhIHZlzcvUaOf7jzzryvuQ/V8jxRT/pyFnlB98rEuS/93kiGp0vxdq10ZF3HJm/QBIGDNLRmZXoESsxkyfo6J+yMjIkrnOspGzcpHuyV7JbV6kybbKOAAAAAJhCgl4IJL45Q/Y8ceZELb8axq2RgOBgHcEt/pygZ6WlSVamsctS4RMgEhgSYv1pNQAA/0ONM8WFj52A4CAJCArSUcFz9fOWzyIg10jQCwESdP/jzwn6tqHD5NSKlTpCfgWVKC61Pn5fAkNDdQ8A4HRx3ftI+lnGmE4oP/wBKX3VFToqWBlJSbLlxlsk4/AR3WNWZItmEjP+CQkIZHctcDYk6IUACbr/8ecEPa7fzXJy8RIdIb9Ca1SXuvO+0REA4N/WtWwn6QcO6Mic6EnjpUz3WB0VoMxM2T50mBz7yp3PhuAK5aX27I8lpFw53QMgJ9zGAgA/Flarpm4BACCS+NEs15LzgLAwqTJ1Esk5cA5I0AHAj4WWL69bAIDCLnndetkz7ikdmVduyL1SrEVzHQHIDRJ0APBjYY0a6hYAoDDLOH5c4gc/KFknT+oes4pdcZmUu+0WHQHILRJ0APBj4TVr6BYAoNDKypLdk56W1C1bdYdZIZWipcr4sRSFA/KAdw0A+KugIAmrUEEHAIDC6vA338rhDz7WkVmBkRES88IzElyypO4BcC5I0AHATwWVKiWBxYrqCABQGKXEx8uukaPtWXTjgoKkwqhHpMj5bK8C8ooEHQD8VFDRIhIYFqYjADArMzNTTp48KYcOHZKt27bJ0qVLJTU1VT+KgpB5KkV2DH5QMo8f1z1mqaNZy/TsriMAeUGCDgB+KqRyJQkICtIRAOSNSrzT0tIkOTlZEhMTZfPmzbJ48WJ57/33Zdz48TJ4yBC5NjZWatetK+fVqyd1rK96DRpI67Zt5bhLiSHOQO07f2qynFq5WneYFd6ooVQeO1okIED3AMgLEnQA8FOhVWN0CwByphLwAwcOyNq1a2X27Nny4ksvyWOjR0u/m26Si9u1k6bNmtlJd6WYGKnXsKG069BBbr71Vnl87Fh5wfpvv5k7V+Lj42Xv3r1y5OhRO6lHwTq66Cc5/P5HOjIrsGhRiZk+RQLDw3UPgLwiQQcAPxVKgTgAOTh27JhcevnlUrd+fYksVkyiq1SRJk2bSq++feX+IUNkwlNPyUcffyzLli2T9Rs2yO49e0i8fUTq7j2yc/gIyUpP1z0GBQRI9JOPS3jVqroDQH6QoAOAnwq/oJFuAcD/UvvDF//+u2zZ6s7RW3BHpvV7jX/oEck4dFj3GGQl51EDbpbSXTvrDgD5RYIOAH4qvEoV3QIAFBb7X31dkn/7XUdmRV7UVCoOHawjAE4gQQcAPxQQESEhZcrqCABQGBxfslT2P/eSjswKrlhBqj43VQJDQ3UPACeQoAOAHwouX04CQoJ1BADwd+lHjkjCsIeshvl95wGhIVJ58gQJKcuNYMBpJOgA4IdCSpaUgEAu8QBQGKhicPHDH5H0XXt0j1ll7rpDirdqqSMATmL0BgB+KKRGNc6iBYBC4sDb78iJ+Qt1ZFbR9u2k4r2DdATAaSToADwlpFxZCa1S2bWvkIoV3Ulkre8RUin6jP8GE18R9evrbwwA8GdJK1fJvqnP6MisEOvzJWbKRG4AAwYFZFl023Fquc2mLrGSujFO92QvatBAiR42VEf/pI6L2NCqvWQcOqR7zqzB2uUSGBmpI/wt8c0ZsueJCTpyVsO4NRIQzD5Xtx2Zv0ASBuR897pEj1iJmXzm33tWRobEdY6VlI2bdE/2SnbrKlWmTdaR/zkVHy9xl3U2flZsYJEiUmfBdxJSJkr3AEDBSkxMlKo1atjHrZmyd9cuiYry9nVvXct2kn7ggI7MiZ40Xsp0j9WRM9KPHZO4bj0lbUe87jEnMCJCqn/wjhQ5v6HuAWACM+gAAACAD9r1xHhXknMJDJTyjz5Ecg64gAQdAAAA8DEHP50tRz/7QkdmlejaWcr06qkjACaRoAMAAAA+5GTcZtkzZpyOzAqrc55UGTeGk0EAl/BOg09R9Qi29rtZ1l3QwvhX3LU9JONEkv7OAAAABS/jxAmJv/8ByUwyP0YJLFZMYp6fZu8/B+AOEnT4jqws2Tf9eUn69XfJOHLU6Fdm8kmpOHqkBBUtor85AKCwyMzMlPT09DN+ZWRkWB9HxurrAjnKsl6buydMylWR13wLCpToMaMkokYN3VF4qfd8TtcF9RjgFKq4FwL+UsX92M+/yI5b7xTrSqh7zCl7/91SYfC9OvIeqrg7hyruvi8lJUXWrl0rf61cKfv375cDiYn6EZGw0FApWbKklC1bVmrXri316tb1fEVpuCcpKUm2bt0qq1avlj179si27dtl48aNcurUKTl58uQZB90RERESEhIipUqVkgb160ulSpWkatWqdrty5coS7EMnm1DF/T98qYr7ke++l/h7hqi7SLrHnNL9+0nlUY8UuiPV0tLSJN4aGyxfsUISEhIkLi5ONllf6rqQnJys/6v/p97zYWFhUrx4cWnYoIF9HahWrZo0btTIvj740jUB3kCCXgj4Q4KeunuPbLZeSxmHj+gecyJbt5QaM9/09F4rEnTnkKD7pqNHj8rcb7+VDz78UH786Sc70cqtOuedJ48/9pj06NFD96AwUMn2tm3bZPHvv8uiH3+Uv/76S1auWqUfdUZ4eLg0b9ZMLrjgAmnVsqW0bNHCHqB7FQn6f/hKgp6SsFPirukumceO6R5zIi5sIjVnvi2B4WG6x3+p17+6wfvLL7/I/AUL5PclS+SYQ89xpJWXtG3TRtpfcom0sK4HzS66yL5OADkhQS8EfD1Bz0xJka3X95eTy//SPeYElS0jtb+eLSFly+oebyJBdw4Jeu78biU169av15EzLrIGKo3OP19HuXPAGkS//OqrMuXpp884k5Fbsz76SLpde62Ozt2sTz6R48eP68h5V15xhURHR+vIGZ999pkcOXpUR867xBqA1vTYUli1HH3Tpk3y2ezZ8smnn8r6DRvsPrcEBQXZN4R69ewpV115pTRo0MCeaTNp1apVsuzPP3WUsxMnTshDI0bYS3RNeXryZClatKiO8q58+fLS+eqrdeQsX0jQs9LSZHO/m+XksuW6x5zgcmWl1uxZElqhvO7xP+o1r27QfWh9Frz73nty8OBB41tXAgICpFixYtKje3fpd/310rx5c+PXgzNRNys//+ILOXLE7KRX6dKl8/U5m1fq53tn5swzroByirrJ0rtXL/sabwIJeiHg0wm69fLc88zzkvjsC1Zb9xkSYF0kq779mhRr2Vz3eBcJunNI0HPn/iFD5MWXXtKRM+675x55esoUHeVMDaZmzZolQx98UBKtgVR+qOWGf/7xh9SvX1/3nBv1sdmoSRPZsHGj7nHet998I506dtSRM5o2a2Yv5Tblnbfflr59+uioYKkVFd9//71Msl5fahCulqwWNDWQU0tfB9x6q/08xcTE2AN2p02dNs1Ouv2NSmo+sBIpEzyfoFvXnF0TJsvBN97SHeaoMV3V11+S4m3b6B7/om7sfj9vnjw1aZKs+OsvV2/YnU6996tXqyaD77/fTvRUMuum2wcOlLffeUdHZqibEbsTElxfMaC2LdU//3yjv9sO7dvLd3PnGrmGKxSJg6cd+22xHHzhFePJufUOkzKDBvpEcg74i4SdO3UrZ4cPH5brb7hBbr7ttnwn54qazVOJEvyPWqr63vvv2zcjevXta88keyE5V9RgcceOHTJq9Gj7Bs+1sbGydNmyAksQfE3rVq10q/BRNXgOzjCbTP2tzF23+2Vyrm7yfvnll9KkaVPp2bu3fW0oyPeeutG7dds2uW/wYKnXsKE8PXVqvlaFnatevXrpljlqlZmqD+O2JUuWGP/d9rFeQ6aSc4UEHZ6VdiBRdj3wsPGZTSWyWVMpf9dAHQFwwxrrg/tsi7jUnuG27dvL7C++cGy5WpkyZew7+/Af6nX0888/S5t27eTmW2+VLVu36ke8KfnkSbuGgvr3XnHVVfYWEuSsRiGtJJ66b78kDH/EyjDNJ5NF2l4sFe69W0f+Q23PurpLF+luJaXqM8VrDh06JA8/8oh9Y/GLL7886+eiEy6xrj2q0KVpc7/7TrfcM3/hQt0yQ60IuC42f8Uez4YEHZ6k9lolPPiQpFsfTKapvVYxz0+XgJAQ3QPADceOHrUrZWdHVc7teOmldlVtJ114wQVG73zDXWof9dAHHpDLrrzSXrLqS9RNJ1XkUN2E6t6zp2zc5MLRWT5I7dNVVfILG7XFM+HhRyTjwP+fTGFKcMUKEjN5ogQY2lNbENSKmslPPy0tWrWShYsW6V7v2rxliz273++mm+x6KyaFhoZKd8NJprJ48WLdcoe6pqoioCapmxvqdBiTSNDhPVlZsu+lVyXpp191hzkqKa80aZyElC2jewC45djx4/by9TPZvXu3XG4lXDt37dI9zimMA31/pWbD2nfsKM+/+KLPLxX/8quv5KLmzWXsE0/YNx3w/0KCg6VChQo6Kjz2v/G2O2OhiAip+vx0vxoLqWMTu15zjTwycqR9PJqvULPnH8+aZc+m/7F0qe4147rrrjN+s1otcTd5SsS/qaMy1VYik27s10+3zCFBh+cc/32JJD7/so7MihpwixS/pJ2OALhJzZ6rs2b/TRX46tWnj5HkXFHVxuH7llqDV7VE3Omj0gqSSiSeGDdOWl58saxYsUL3om7duoXuaKrjfyyVA9Of05FBgYFSYfhQKdKkse7wfStXrrSvDQt8YNY8O3v27rVvUs98911jS95VXQd1DJxJu3bvNp4wn27evHm6ZUZERIR9yoppJOjwlLT9+yXh/gftJe6mRTS/SCoMvU9HAAqCutt9OrU8bcgDD8iSP/7QPc4KCw0ttHtZ/Yk6r/iKq6+W/S5U3i4IaltH3379XJ158rKaNWvqVuGQfuy47Bz+iCs1eIpfeZmU6Xe9jnyfWlLdvlMniU9I0D2+S92sHnjnnTJt+nQjSXqRIkWk2zXX6Micb13ah66eo3k//KAjM9TpKiVKlNCROSTo8Az1QaQKobix1yooqrTETJ1k/Ax3ADn77V/709TxN2rGwJSyZctKKcN7x2DW+vXr7RUWJs+h94LJTz1l7xOFSLOLLtIt/6eOQU0YOUrSEnJ3ykV+hNauKVUmjpOAQP9IB3788Ue5qksXv9oioqrPjxg5Up559lnd4yxVjdy0xS4VwTyVkiJ/GLq5/7cBt92mW2aRoMMz9r/+piT9+IuOzLH3nU8eL6GVonUPgIJy+hL3o0eP2kfOqAGJKeXLl7cLTsE3qWrHsT16yIFE8zdyC9KdAwdKVyvRwH80bdpUt/xf4rvvy/FvzM84BhaJlJjpUySoSBHd49vUqqtu3bvbs87+Rq0sG/bQQ/Lee+/pHue0bNlSihcvriMz1LFnKVbybNrmuDjZu2+fjpynzqpv17atjswiQYcnnPhjqeyfPF1HZpUecLOU6NBeRwAKkpoN/dsbb75p/AicphdeSAV3H6WWL6qCT1u2bNE9/kkVMZwwfryOoPaeV6taVUf+LXnNWtk7eaqODAoMkIpjRklk3bq6w7dt375duvfo4ffFFe+8+27Ht3+pauRXX3WVjszYt3+/7Le+TPtm7lzdMkMtb3friFYSdBS49EOHJGHIcHWLUPeYE9mimVQcwr5zwCvUHmK1VDkxMVHGjB2re81RxabgmxYtWiRvzZihI/+k9oS+/eabUrRoUd2DkiVKSFRUlI78V/qxY7Jj8IOSddJwxfGAACnd73qJ6nat7vBtasa8d9++dhLo71QRyRv69bNXEjnJdFVyNXv+7+1sTlOrDEzudVc39u+4/XYdmUeCjgJln3c+bISk796je8wJKlVKqkyfzHnngIeo5ez79u2T995/X5JzOBPdKer8Uvge9ToZNXq0PQjzV2oAOOKhh6RJkya6B0r5ChXsysl+LStLdj05QdK2/bNopgnh9etJ9EMP2om6Pxj75JOy3MUTD4KCguxio2plh/pSW6ZCrHGlWyuzdsTHy52DBjl6LWzerJmUKWP2iL1vv/1Wt8w4euyYrDttRZ7ToitWlGbW8+QWEnQUqP1vzZATC37UkUHWhTN6/BgJLYTnqAJepqpUr9+wQV562fzRimogVa1aNR3Bl6iq7aYq+3uFOvJo6JAhOsLfGjdqpFv+69CXc+ToZ1/oyJygMlFS9aXnJNBPjqz7+eefZeq0aToyRxVrvOLyy+WF556TX3/6SbZt2SJ7d+2yv/bs3Ckb1q6Vb+bMsW+w1a9XT/+/zPnK+l5OzharquQdDB8/+rN1Dc/IyNCR89TJF06vLDhd+/btjR9JdzoSdBSYE38skwNTntGRQVZyXvq2/lLyyst1BwAvefOtt2TL1q06Mqdq1aqufsDCGWrv+bPPP68j86pUqSI333STPD15ssyzBsEb162TrXFxsm/3btm0fr2sW71a5n//vTz3zDMycsQIuaZrV6lXt64E5+NUkKjSpeWdGTPsmTj8U+3atXXLP53avkN2jx5rz6KbFBASLJUnPCFhflIgV+03HzBwoI7MULPivXr2tN/zc778UgbefrtdsFCdBqK2o6gvtSc5JiZGLu3UScaOGSPLly2T2Z9+Kuc3bKj/FuepFUX3Dx7s2J579XPecL3Zo/ZUYc+9e/fqyHnfWddkk9xc3q6QoKNApCUelIT7H3DnvPMLm0jFB5mVALxqztdf65ZZlaKj85VEoWAcPHhQfvr5Zx2ZU716dXlv5kw7IX/t1VflvnvvlfaXXGKfm6+SdlXBV/03KmFs166d3HnHHfL46NHy6axZsuLPP2VXfLx8/OGH9kC3SuXK+m/NnSmTJkmM9T3wv/x5W0pGcrLEW2OhzOPmi5tF3XaLlOjYQUe+b9KUKbLVYFHR4lbiPXPGDHn3nXfsm7u5pZbAd+ncWRb/+qud0JuyfccOef6FF3SUf5dY1zpVMM6UZOu1/qd1nTRB3cT94gtzK1DU7/8il496JEGH67IyM2XX6LGSvtfcUQh/U8u5Yp6dKoEcqwQUem5/wMIZq1evto/gM+ni1q3lj8WL7dmyvMxiq0G5SuBju3Wzi7ytW7NGflq4UHr26HHWI4xu6NtXbrjhBh3ln7pxsNMavOfma9WKFcbPWl/1119n/N65/VL7Y/2RGgvtfmqKnFqzVveYU6RNa6k49H4d+T61D9vJ5PTf1EqrD99/X3r36mXPLueF2lL17PTpcv+99+b57zibqdbf79S1Ua0GuLRjRx2ZMX/BAt1ylqoQb/J0j8svvdT11U0k6HBd4tszXTnjU4KDJHrCExIaXVF3ACjMateqpVvwJaZnz1Vi/cF77zk6e6SKR7Vq1Uref/dde3nsxPHj7RUc/x6oq5n2p59+2tEBvEou1Hn/uflSS3VNK2d9jzN979x+qZsf/ujoDwvk8Acf68ic4HLlJGbyRAnwo+dxivWeUad/mDJ61Ci57LLLdJR36rU7ftw4Y6tADh8+LK++9pqO8kddg9QNSpN++fVX3XLWXytXGi0ya7rK/ZmQoMNVSStXyb4p5gt6KKX69paSnfxnOReA/FGzpPA9JivzKpdbA/GKFc3dyFVJ5gNDh9qz6mrfutqvqqhK0G++/rq9/xyFS8quXbJzxCgRg0WzlICwMIl5fqqElDN/I8YtO3fulLcNHrfYonlze3uLU9QKlReff14iDZ1E8Jp1DVF70p2gbkoUMVinZc3atfZNBactMDQzr6itR82t14TbSNDhmvRDhyXh3qHmz/i0hDeoJ9GPPqxuCeoeAIWZWm4Ycw77COEda9et0y0zqrtU2V/NbN8xcKC9rHzUyJEyePBge98nCpdMfbxs5pEjusecwKJFJMzPTq6Y+e679nngpowZPdrxWiWqbsWtt9yiI2dt275dFi5cqKP8KVq0qHTp0kVHzlNHw6lq7k5S+89NFojr2rVrgaziIUGHK7LS0yXhoUckLWGn7jFHfSBVeX6aBBreVwegYKgjYa6+8koZ/+ST9pE3CdYA5ejhw3LM+jp66JBs37JFfrMGAWr/31133GGfK93m4ovtGUv4nsTERN0yI82FYqWnU3s9Hxs1Sp4YM8bY3lR41/6XX5PkJUt1ZFbGwUOy8/En7f3u/uDkyZPynMG9561atpSOhvZh33P33cb2Mb/l4IoCVTfDpCVLluiWM/bs2SMbN23SkbOCAgPl+r59deQuEnS4IvHDj+XE/EU6MicgOEgqTZ4g4Zx1DPidqKgoefyxx+wq2198/rkMe/BBe+lZhQoV7OWDEdaXmqWsVKmSNLvoIrnrzjvl2WeesYt/fTF7NskQzihu82Z7FsZtvB4Lp+AyUbrljuNzv5NDn3+pI9/2w/z5cuDAAR0575abbzb2vqxmjUsbnX++jpyl6nQkJSXpKH/atW1rf5aaov6tTl5vf7cSfqeW+P9b5cqVpemFF+rIXSToMC55zVrZN26SWoeie8wpqfadX5H/wh4AvEMNl9SxNSuWLZORjzxiJ+rnQg241BJ3+CbTieyChQtlm8HjmoDTRfXsLpHNmurIBdbYa8+TEyTNYGLrllmzZumW89QKq64Gl3erZdLXxcbqyFn79u2TpUudWZVRqlQpu2q5KWofempqqo7yb9Eic5N/3bt3L7AilSToMCrjxAlJGDpcsgzuF/pbWP26Ev3IcDWa0z0AfJ36cLz//vtl1kcfGS3kBe8yfcyWqgZ9ddeukpCQoHsAcwKCg6XSE49LgIvHNmUePSY7R41xZaLElJSUFJnzzTc6cl4z6zpTpkwZHZlxmcHE9+NPPtGt/OvVq5duOe+ElRcsX75cR/mnbrCaoG7Y9L/pJh25jwQdxmRlZMjOkaMlNc7c2YR/CyxWVGJemC6B4eG6B4A/GHTnnTJp4kTHi/bAd1RzobifOkO3WcuW8sGHHzo6uwOcSUTtWlL2vrt15I7j8+bLQR9e6v7rb78ZPVqtk+EzwJW6devqlvNU8TVVhM0J6lg4VSvDFLVVwQm7du82tv+8Zq1acl7t2jpyHwk6zMjKksT3P5RjX5m72/lfgQFScexj7DsH/Eznq66SyZMmGV/iDG9r1KiRbpl18OBB6X/LLdK0eXOZ9ckncvToUf0I4Lxyt/aXsLp1dOSOveMmSuqevTryLV9//bVuOU99xnRo315H5oSHh8v5DRvqyFm7rWT10KFDOsofdTRkG4PHkqrz0J3Yhz537lzdcl732NgCnRggQYcRyes3yL6JU9zZd96zu5S+tquOAPiD0qVLyysvv1xg+7/gHepcYrdu0qhB44YNG+T6fv2kboMGcu/998uyZcvs5bWAk9SKv8qTxkuAi6dLZBw+IgmPjLJXOPoSVQTsx59+0pHz1PFi6ig009R1rGKFCjpy1rFjx+yCl07p37+/bjlv06ZNjqxUMra8PSxMbr75Zh0VDBJ0OC7j+HFJuGewZCWf1D3mhNaqIZVGj2TfOeBH1CDmybFj7bv4QI0a1nU+OlpH7lHHu738yivSqk0badSkiTwwbJhdiMntY9ngv4rUryel+/fTkTuSfvlNDs3+Qke+QS1tX79+vY6cp07/KFmypI7MKmHw+6xZs0a38q/9JZdI8eLFdeSsnbt2yfbt23WUN+qm6dJly3TkrAb16xfIZ87pSNDhrKws2fX4k5K6bYfuMEftO6/68vMSaPA4CADuU/v0buzn7qAV3qUGz7HduumoYGzdtk2efe45ad22rdSoVUv63XSTfDxrll09GcgzNaM6+F4JqRqjO1yQmWlXdU/Zbn6c5hSVzKUavDGm9hqHurSSoVzZsrrlvN8WL9at/FOnpbRs0UJHzpv77be6lTfxCQn5TvKz0+3aawt89R4JOhx1cNancnS2C0VIrDdOxdEjJbxmDd0BwB+o2fOHhw+39+oBf7vn7rs9c1TeXisp/+jjj+WGG2+UKtWqSYtWrWTM2LGy6McfjRaxgn+yl7pPeMLVlYCZx09IwqOjfWapuyqAZlLVGPdukJQrV063nLdnzx7dyr/AwEC5yeCN8vxuWfjhhx90y1mhISFGl/fnFgk6HHNy4ybZM/pJV/adl4i9RkpfV7AzKgCcV7lSJfvuNXC66tWrS4/u3XXkHWrP+vIVK+TJ8ePl8iuvlErWQL9Xnz7ysZXAq8GyE4WQ4P+KtWgupa7vrSN3JC9eIgdmvqcjb1OnLJhUqnRp3fJtcXFxuuWMK664wtjKgpUrV+a5toe6rn5j6Mi9pk2bGqsTcC5I0OGIjKQkib/7fnfOO697nlR+YjT7zgE/1Kd3b3tJM3A6tbJizOjRUqxYMd3jPWrQePLkSZn9+edyw003Sf2GDeWKq66Szz77jJl1nFWFwfdKkOFzuP9t/9Rn5JQPLHVXN8FMenvGDKleq5YrX09Pm6a/q/MOHjokpxwch5coUULatmmjI2eplUjqmLS8UNfTPw29Jq695hr786agkaAj37IyM/+z73zLNt1jTkBEhFR55mnOOwf8kFpaNuC223QE/FPVqlXliTFjPDF4yo0TSUmycNEi6X399VKvQQO7yNyOHTuYVccZhZQuLZWefFytLdY95mUmJcvOh0dKVnq67vEeVZTRyaXbZ6ISvp07d7rypaqtm6LOQVc3CZ2irrU9e/TQkbPU71UV3cyLzZs3y4EDB3TkHPXz3mBdr72ABB35dviLr+Top5/ryCDrjVPxsREScZ75ozAAuK9+/fp2EgZk546BA+Xqq67Ske/Yt3+/XWSuVp060veGG2TFX3/pR4D/V6JjeynWqYOO3JG89E858P6HOvIetQz6FMcc5k5WluOnTJicUf5qzhzdOjfzDO0/v7h1a6nggeXtCgk68kXtO989YpR9UTCteLeuEtW7p44A+JtOnTpx7jlyFBwcLDPeeksuaNJE9/ieTz/7zC4spxJ1dR4w8LcA6/pXeexoCSrlzpFff9s/aaqc3OTs/mWnqPOy87pXubDJyMx0fIa+TJkycvlll+nIWUv++EMy8lCo8Lvvv9ctZ/Xt00e3Ch4JOvIl/t4hkpWSqiNzQuvUtj60HrNn0QH4p+s99OEI71L7Ir/+6itp0rix7vE9apn7J59+Kk2bN7crwCcnJ+tHUNiFlCsrFR59WEfuyDx5UhIefFgyrWTYa1TCefToUR2hIPTp1Uu3nLVv71572f+5OHjwoPy5fLmOnBMREeGpArUk6MiXNJfOO495dqoEFS2qewD4m0rR0VKvXj0dATkrW7aszPvuO7n80kt1j29SBZ1UBfiL27a191UCStQ110jRDu105I5Ta9fJ/ldf15F3qJtZ1G0oWB07dpSQkBAdOeekdf071+0+a9audXSf/d/aXHyx0SPwzhUJOjyv/MMPsu8c8HNNmjQxMgCA/ypZsqR89umnMuT+++0ze32ZGnS2veQSmfvtt7oHhVpggESPGikBLp/9f+DFVyXZStS9JN1Hzmr3Z9HR0XJxq1Y6ctbXX3+tW7mzaNEiIzdsbjR45ntekKDD89JU9U7ungJ+rRrF4ZAHYVYCM+mpp2TWRx9JlcqVda9vSjx4UHr06iXvvf++7kFhFl41RsoPG+Lq1r6slBTZOfIxyXK40Fh+sP3DG/r27atbzjrX5erz58/XLeeobVOm9tnnFQk6PO/gq2/KsZ9/0REAAP90Tdeusuqvv+zZdDXY8lWqINaAgQNl1ief6B4UZmVu6CvhDdzd+nNq9VrZ+8JLOip4xdjemGtqJVFkZKSOnNWhfXv7hqjT1Oqh/fv36yhniYmJsvTPP3XkHHXWe1RUlI68gQQd+RLZoplumZOVmiY7HxwhqbvNnoMJAPBdRa2BvJpN/8sawPXu1cvYQNW09PR0uf2OO2T5ihW6B4VVYGioVJk0QQLC3V3qnvj625K0arWOCpapI778kXqm1EkXJqgjUBs2aKAj56jl6r8tXqyjnC3+/Xf7+ui0/jfdpFveQYKOfKky9SkJKmP+rlPGgUSJH/yAJyuMAgC8o3LlyjJzxgxZuXy53HvPPRJVurR+xHckJSVJ/5tvZnkv7Bo8ZW6/TUfuyFJV3Yc/Yld3L2iqNgn1SXInwOAMupqdv7l/fx0567ffftOtnP3000+65Zzy5crJpZ066cg7SNCRLyHWC7vKM1MkIMTMHbvTnVy6XPY9+4KOAAA4MzXrVq1aNZk6ZYps2rBB3nrjDWnVsqVPzcZt2LhRJkycqCMUWtZrtvydt0torZq6wx2pcZtl74sv66jgqISzSJEiOkJOVBIdGhqqI+d1vvpqCTPw96uZ8dwUfvs1l4n8uVDV29XqK68hQUe+FWvdSqKsDw83JL78uhz71fk3KADAPxUvXlz63XCD/LRokWxct04mjBtnD8pMDDSd9tIrr8i+fft0hMIqMDxcKk94UiQoSPe4Q425Thg4c/pcqH3P4S5Xs/dV5cqWNZqgq2rujRs31pFzVq1aZR85mRN7//myZTpyTu/evXXLWwKyDB4umJWeLpu6xErqxjjdk72oQQMlethQHf2TWta8oVV7yTh0SPecWYO1yyXQR/ecmZT45gzZ88QEHTmrYdwaCQgOtn/X2269Q5J+/lU/Yk5wubJS66vPJMT6s7A6Mn+BJAwYpKMzK9EjVmImn/n3npWRIXGdYyVl4ybdk72S3bpKlWmTdeR/TsXHS9xlne3XsEmBRYpInQXfSYgLW0JMuH/IEHnxJXOFg+6+6y6ZPm2ajrxNfWw2atLEnuE05dtvvpFOHTvqyBlNmzWTVavN7St95+23pW+fPjryNvU7PHbsmHwzd64stBJ3tcRyy9atRvY35tewBx6Q8ePG6chZatBbtUYNuzidKXt37fJcAaZ/W9eynaQfOKAjc6InjZcy3WN1dO52TXhKDr7+to7cEVI1Rs6bM1uCCmh8rd6TdRs0kB07duge56nVNu3attWR7zqvdm15aPhwHZnx0ssvy32DB+vIOQt/+EHatGmjo//1yaefSt8bbtCRM9S551s2bZLw8HDd4x0k6IWAGwm6knbwoMRd3U0y9pv/kIts3VJqvP2aBBTSfUkk6M4hQc8dEvT/R4J+Zr6UoP+bSgLUTLVK2NWXmqlRlYUNDpFyTc1aqZl/E4NIEvT/8JUEPeP4cdl49bWS7nLR3NL9+krlx0fZy+0LwiUdOuS6kFheXHXllfLl55/rCDlR18VqNWtKmsNH8Y146CEZO2aMjv7XgNtvlxkzZ+rIGX1697brlXgRS9zhmBDrA7jylImuLMFKXrxE9r7wshop6x4AAPJGVT6uVKmS3D5ggMz+9FPZsHat/Pzjj3LXHXdI1ZgYY5WRc2Pv3r2yctUqHaEwCypWTCo9aSUxge4O3w9/OEuO/1lwS92bXXSRbpmxes0aycjI0BFyUrZsWWnZooWOnJPTPnR1A3HxkiU6ck6P7t11y3tI0OGo4m0vljJ3ubAf3XoTH3zxVTn+x1LdAQCAM1TRoBbNm8uzzzwj661k/aeFC2XQXXcVSEX4zMxM+frrr3WEwk6Ns0pc01lH7lArzHYOf0QykgrmVIHatWvrlhknTpywt7zg7FShzWu6dtWRc9SKtJSUFB390549e2TLli06ckb58uXtlRNeRYIOx5W/Z5BENDd7t1PJSkuTnfc9IGkuLKkHABRO6oinZs2ayTPTpsnWzZvlpRdekNq1aulH3bHkjz90C4WdOkor+uFhEhTl7s2itB3xsnvi5AJZudjCwIzt6VRyvinu7Ntx8R/XXnut4ydiqJVC27dv19E/qTohTq9w6NK5s9GCevlFgg7HBYaFSswzUySobBndY066lZwnDHtYstJZmgQAMEsd+TTgtttk5YoV8uTYsRIREaEfMUvtiWcJLv4WUrasRI8e6fpS9yMffyLHfjO3Fzw71atVM3rUmlql8sMPP+gIZ6N+H82bNdORc+Zks1LI6RVE6ji63j176sibSNBhRGiFClL56Yn/LSBnUtJPv8q+51/UEQAAZqlZdVUt+fNPP5UiLhSnVUXsfHUJrhcr4/uDkldeIcUudbaQ5NnYS92HjZD0w4d1jzvUlpP69erpyAyVHHqhKKSvMFEQ9EyFAJOSkhzff17RylFat26tI28iQYcxxdtcLKXvuE1HZiWq/ei/swQQAOCeDh06yPBhw3RkjprhU/tknaaK3zm9VPXf2NtrRkBQkFR6/FEJLFFc97gjfd9+2TV+kqtL3YOsn/XSTp10ZMZfK1fKtm3bdISzueLyyx0vnrl8xYr/OQ9dHX+pKsc7qVu3bvb5+l5Ggg5zrA/9ivffIxEtnV8G82/2fvQHHnL9ri4AmKASMiepmSGnj8XBfwom3XbbbcaXuqvf378Hrk5Qg1TTCbrJI9wKu9Dy5aX8g0N05J6jn38pRxf9qCN3XHHFFbplhlrp8ZZHj9zyoho1akjDBg105Ax11OWu3bt19B+LFy92dGWDutnTz+Hz1E0gQYdR6pzyKpMnSlCpkrrHHHUuaMJDI42fZw0Aph0/fly3nPHJp5/K+g0bdAQnlS5Vyt6T6YtMJ+fKZoerL+OfyvTqKRFNL9CRSzIzZddjYyX96FHdYZ46VUEd8WXSjHfesZdU4+zUPu4b+/XTkTPUTZLffvtNR/8xZ84c3XJG1apVpdH55+vIu0jQYVxY5UpSaepT9nIs007MWyD733hbRwDgm5xc0hcfHy/3DR6sI/9y8OBBOXCgYE/yUANVtSfdNDXz47Tw8HAJNJykq2WrMCcgOEgqj39CAiLCdY871KTIzkdH28m6G9Ry6l6GC3up47zGPvGEjgqe0yupnKaOKXO6Evrcb7/Vrf/cqP7lXwl7fl17zTWert7+NxJ0uKLEJe2k1K036cisA1Omy/HFzhaUAAA3rXAoqVH7f/vdeKMkJibqHv+hkvOu114rzVq0sCswF+Rg1vRuXJWcFy9uZq9x48aNdcsMNSNG8S2zImrVlHL3DrK3Frrp2Lffy5H5C3Rknqq8bXrVx6uvvSZr163TUcFQ25Heffdduenmmz1dZLFatWpSvXp1HTlDFYr7+2detWqVoysa1M3UW/r315G3kaDDHdYFteKQ+yS8SSPdYc5/qow+LOmHj+geAHCOGiCWKlVKR2aoY7Xym9SkpKTILbfe6ngFXC9QMyvXxsbaz5Pas9jFStT733KL7P7X/kU3qL3hpm+AhAQHS4kSJXTkrMqVKumWGcv+/NM+4xhmle1/o4TVq6Mjl2Rmya6RoyXNpVUszZs3N17N/YSVEKqbmkddXL7/N3XNX7lypVx6+eVyy4AB8vGsWfLsc8959gaXWjnU7/rrdeQMdeN1165ddlsl607+7OfVri21rS9fQIIO1wRGREjMc1Ml0NAswOnSd+35z/noHl8eBMA3mS4KtnrNGlm7dq2Ozt3JkyflZis5/9Lh/XteoKqZ9+7bV5b88f8nd6gzwj/86CNpfOGF8tSkSUYqnmfnp59/Nn5joEmTJsaW0VcynKCr38XjY8bkeaDNMW25ExgeLlUmPGnX/nFTxsFDsvOxsSq71D3mqJUkQ1zYrrPGuvb26tPH8VogOVFJ6W233y4tL774v8eNqffMqNGjZf78+XbsRdfFxjq6/Ubd8FQ39RSnz6bv0qWL45XnTSFBh6vCKleWSlMm6MisE/MXyYF33tMRADjH1Gzm6cZPnJinpEYN9GK7d7cLw/kbNWDue8MNMi+bgduRI0fk0ccekzr168u06dONz2yr/e+Dh5ivon3hhRfqlvNat2qlW+bMfO89eeONN87p9bxp0yYZPHSotGvfnkrwuRTZoL5E3eLOdsLTHZ83Xw598ZWOzFIJYaXoaB2Zs2DhQmnTrp2sW79e95ixdds2GTZ8uNRr0EBmvvvu/9yQUq99tTpo+/btusdb1BL3enXr6sgZ38+bZ2/P+vHnn3WPM26znkdfQYIO15W8rJNE3TlAR2btnzRVklat1hEAOMPp42XORCXYL738cq6TGjXz8N7778tFzZvL/AXu7Qt1i1qyP2DgQPn2u+90T/ZUkb3hDz8sNWvXtgvkLV261PFj5rZZA+bOXbvaA2yT1L5Jk8WxatWqZX8Pk9Rzf89999mJxurVq8+YcKvX70YrKX/nnXfkyquvlkYXXCAvvPiivY1BJS7IhYAAqXD/PRJavarucIl1jdoz7ilJdWErQ7FixeTRkSN1ZJZKzlu2bm2vyjns4DG+aoWTmhVXK4HqN2wo0599Vk7mcIzi/gMH5NrrrnN1ZVBuqZU9vXv10pEzVN0Kdc12sq7IBdb1RB0N5ytI0FEgKgy+T8IamN1HpGRZF8GE+x7gfHQAjlLFcUxTifnQBx6QIUOH2rMnahn3v6k+dXasKijUtFkzueW22yTx4EH9qP9QyZv62T6bPVv35E6y9RmgbnK0bd9e6jZoII89/rgssxI+NTuTl9UJasCoKj1PfOopaXLhhbLir7/0I+ZUrlxZzrcG8aaoGbCSJc0fhZphPXcffPihNG/VSirFxNhJuNqG0cdKUlpdfLFUrlpVLrCe09sGDrRvMJ3+ele/N7U3FWenlrpXGjdWrQfXPe7IOHRIdo4cLVkZ5rcWXn/99UbfE6dTybRalVO3fn15cPhwWb58+Tknyuq1rFbzqO0walVInXr15KouXezr2Zmu62eybt06ueOuuzxZ2V0l6E7e5NuwcaN89vnnebpGZ0dVbzd9I9JJAdYP79xP/y+qWNemLrGSujFO92QvatBAiR42VEf/lJmaKhtatbff/DlpsHa5BEZG6gh/S3xzhux5wsyy8oZxayQgj/s5Tm3bLlu6XieZScm6x5yiV14m1Z6f7spRb25QVVMTBgzS0ZmV6BErMZPP/HvPsj4Q4jrHSsrGTboneyW7dZUq0ybryP+cio+XuMs6Gz8/P7BIEamz4DsJKROle3zL/UOGyIsvvaQj591tDTymT5umI+/7a+VKad6ypaMDiJyo47BqWImUOgv472RKLef+fckS2blrl6t7JbPzzttvS98+fXTkHDVzrhI5p5bsqyJ/ZaKi7JssHTt0kPPPP98uPFW6dGm7UrraT6m+1MBZfamZM1WI7pdffpHvvv9e/szDAD0/Hn3kERltJQgmXdutm3xz2vFGXjT4vvtk8qRJOnLWupbtJN2FQmfRk8ZLme6xOjLIui4lPDZGDr//ke5wifXeqvTUOIly4Wf82Xo/qmJqBZGwli9f3i441rJFC6lQsaJ9bVbXjrDQUEm3rhlqmbq6qaqKI8bFxcmvixfLgf375eixY/pvyLunJkyQoS5sqzkX6nfQuk0b+9rolL+vwU5Q1/y1q1b5TIE4hRl0FJjw6tUk2rqQiwt3tE58O08OzJipIwDIHzU4i7CSZreoGWS13PKtGTNk2jPP2F+qvX7DBk8k56aoga7a4+3kfnp1U+VAYqK9dPqpyZOl3003yYXNmkm1mjWldNmyUrVGDXvZaYyVwKu45nnn2fugH3n0Ufnxp59cTc7VTYN777lHR+b0MXBjxWkvvfKKbLKSHeSCWuo++F4JKldWd7jEem/tnThZUvft0x3mXNy6dYEdmaVWLakbBJOffloeePBBu+ZHp8sukzaXXCLtO3a0bxyo7Thq5n3GzJmyefNmR5JzZfTjj9v7471EzUx369ZNR85wKjlXLmjSxKeSc4UEHQWq1FVXSKm+zu5dyc7+ydMleW3Bnm0JwD9ERkZKhw4ddAQTVHJ+/+DB8vqbb+oed6iVCfEJCY4NqPNj0J132km6aZd26iTFixXTkTeplRQPPfywa6tWfF1IVJRUGjPKlUmQ02UcOiwJwx8xvyrN+rmenjLF8QJlXnfKeh/c1L+/7NixQ/d4w5WXX27PVHvRDQ4fBecGEnQULOsCGz1qhISfb77gUtapU5Jw71DJOO69IhsAfE/PHj10C05TSyYfHDZMXn39dd1T+DSoX18efughHZlVpkwZueyyy3TkXV9/840s+vFHHeFsSlzaSYpd3klH7kn6dbEcnGX+FIkiRYrIzBkz7MJxhcm+/fvtWXsvrZ5q1KiRJ4uwhYaGSteuXXXkO0jQUeACw8Ik5oVnJLBYUd1jTuq27ZLw0Eh7DzYA5IeadVQDRDhLLW18ctw4efHll3VP4aMGla9aP3+Y9fnoBjXz9cjDDzt6nrEJavZ8+EMPcexaLgUEBkrlsaMlKMr8Kox/sH5Pe8ZPklM74nWHOY0bN5Y3X3/dfs8UJqvXrJFB99zj6FLw/FArGkzUIMmvphdeKNWqunyqgQNI0OEJYVUqS/STj9sz6qYd/26eHPzwYx0BQN6oQkGxDu+7My0qKkouaddOR96jErBJkyfLuAkTCu1SZpUkPzNtmjRv3lz3uEMVy+vapYuOvEsVaHxnJjVlckstda/w0IP2vnQ3ZSUny87hIyTLhSJuqkL3+Cef9OwSa1M+/OgjmfL00zoqeF07d7YTdS/pf+ONPvm6IEGHZ5Tq2llK9rpORwZZHxZ7x0+S5HXrdQcA5M2wBx7wmZkb9e987eWX7UrwXqUGUl2sQV6tmjV1T+GiBrcPDx8ut916q+5xj3ruxz7+uGuz9vnxxLhxdq0A5E5U7LVSpO3FOnJP8rLlcuCtGToyR712VTHFxw2fduBFU6dP98wRhOomX6XoaB0VvKJFi8pVV12lI99Cgg7vsC6w0SNHSFgd85UWs5JPSvxd90mGH1c/BmBevXr17Dv0vkANXtVevJiYGN3jTWqQ9/vixfbZuoVpRiw4OFhGPPSQPDZqVIH93Or1rI518/rzvnv3bhk/caKOcFaBgVJp9EgJiIjQHe7Z/+yLkhJvfqm7urk14uGH5ZWXXpLIAvg5C4Javr1w/nx7ZZQXhISEyI39+umo4F3UtKlUrFhRR76FBB2eElS0iMS8+KwEFjdf8CMtPkF2jhxt75UCgLxQicyTTzwhVSpX1j3eo/6NI0eMkAcfeMCO65x3nv2nlxUrWtQu/vTBu+9KxQoVdK//UufcPzt9un3eeUEvEX1g6FDp0L69jrzrRSsR27hxo45wNuHVqkn5YUPsyRA3ZZ44IfFDh0umC3UD1LXu1ltukdmffSZly7p8xJyLSpQoIU+OHSs/LVok9evV073ecF1srGdqWVzft6/nbzZmhwQdnhNeo7pUfMJKnF0YpBybM1cSP/hIRwBw7tQxWK+/+qonl7qrWVk1I3r6rGy5cuXsP71O/Xu7d+8ufy5dKjf16+cTS6/zomrVqvLNnDly+4ABnhhMqlmw92bOlEbnn697vMk+dm3EiEJbqyAvyvTpLeEN6+vIPSf/WiUH3npHR+Z17NBBfv/1V/usdF9N0M5EJb6XXXqp/PnHH/LQ8OGe/MxRq3C8MGutKvur2gS+igQdnlSqy9VSsqcL+9GtD/Z9456Sk3GbdQcAnLuOHTvK1ClTCnz283RqmecLzz4rj44c+Y9/V6lSpXxq0Kpmwl5/7TX5+ccfpXWrVp56jvNDJcK39O8vS377Tdq2aaN7vUEdu/bF7NmeT9LVsWvz5s3TEc4mMCxUqjw1XgLcvtlljbUOPP+Sq2MttZXnu7lzZdwTT9h7kX2Zul43aNBAvvjsM5nz5Zf2TT2vUjcNenbvrqOCo66p6rPOV5Ggw5PU0SDqfPSwuuaXYmaq/eiD7peMpGTdAwDnbuDtt9uVhL2QQKol93O++kpuvfXW//n3qJkFVYHel6gB6gVNmsiCH36QL63EUSXqvkztjfzeSh5eefllz+wf/bfK1mtIJThqNtKL1GtCnUhQycPbS7wo4rzaUmag+0UIM5OTJWHYw5KZlqZ7zFOrboY9+KD8sXixdLvmGp+cTa9bp468/cYb9o28K664widuUHrhuDVfr2FCgg7PCipSRKpMmyyBLpwznLp5i+x6bIxd4R0A8kINBtT+3bdef93eQ10Q1L+h3/XX28vCs5uVVTMcBfXvyy+1xFMNUhctWCAL5s2TXj17+sxZ9Op3oxLz9999V379+WdpY/1+vD6AVDPpasZOFa8L99AWA1Uc64P33pPvv/1WGtR3f8m2T7Nec+XvulPCrETdbadWr5V9z72oI/fUrl1bZn38sfy4cKFccfnlnj/vX1HL8z98/31Z8eefcr11TfelLT5qmXv16tV15L7ixYv7xJGROSFBh6dF1K0jFceOsl6p5l+qRz//Sg7O/kJHAJA3ajC1dMkSV88bV3vN27VtKz8vWiRvvvFGjkv71NJqNYvuy1Ri29b6edVe6a1xcfLUxIly4QUX2M+D16iCTj2uu05+XLDATsx79ujhU8v01etl7JgxsmTxYunUsWOBPceqkJ76/p998on9PHa3nlN/2e7gNrXUvdK4MerCoXvck/jqG5K8vmCOuW3VsqV89cUXslzXtSjjsdUrau+2OmLxLyspV6uF1Gvci9e0s1Hv1dhu3XTkPnWd8PnPuCyD1TWy0tNlU5dYSd0Yp3uyFzVooEQPG6qjf1KVHze0ai8Zhw7pnjNrsHa5BEZG6gh/S3xzhux5YoKOnNUwbo0EGL54ZGVmSsKIR+Xox5/pHnMCrIFIza8+kYg6dXSPNx2Zv0ASBgzS0ZmV6BErMZPP/HvPysiQuM6xkrJxk+7JXsluXe2VDP4q7cAB2TVuovWcmF09YQ+IHntUgl04ocCEGe+8I/OsAYMpl3bqJDf3768j/5Bhvc++mjNHxowdK+s3bLBjpxWxPvNUovrIww9L8+bNcz0zpCpg/2YlXE4adOed0rp1ax25Tz2/O+Lj5QtrAP659bV23To5evSoftQ96uZByZIlpdlFF9mJeTdroOrLeyFPl2l9Hq9YscI+4mzBwoVy4sQJ/YgZatawRo0a9p7W/jfdZC+7N5GUJ4wcLenHjunInKjr+0jxVi10VPD2v/m2JK1YqSP3hJ9XWyrec5c9m1+Q1PVh7ty5MmPmTPlz+XI5fPiwfsQd6nqtiox2uOQSOzFv2bKlRPpJHqM+88aNH6+j/3UyOVm+tp57E5+L6satWl3ly0jQCwFfT9CVDOuDc/N1vSV1yzbdY05Y7VpS68tPJDA8XPd4Dwk64DvS0tLkj6VL5ZVXXpG5334rx62kJq+DkkBrQKtmNFUyrgYgna++2k5avL5U2m1qaHPw4EFZvXq1fPvdd/bge8kff0i6NS5RX05SM1xqoK0S8hbW7+Vq63eiiqupJN2fqbPIv7EG2LM++cR+bk+ePGkn8PmhXttqxUFrK1FRS1TbtWtnF8TyhSXJ8G1HjhyxrxOq8ODixYtl9Zo19rXCyQRSXSvUlhx1rbj8ssukffv29h7ziEJybvvpPps9W3r37asj56jnd1d8vM9sfcoOCXoh4A8JupK8br1s7XmDZCWbL+ZWsncPqTLhiQK/u5sdEnTAN506dUrWWAM/lbD//vvvEp+QIIlWIhlvDShUgnM6tf+3fLlydhExVfStWbNm0rBhQ2ncqJHfJ38mpFpjiZ07d8qatWvtPzfFxcnWrVvtgbmaSUtKSrJn4M9E/Q6iSpe2n3e1v7GalTTWq1tXqsTE2L+TypUqFcpB9t/Uc7fWel5Xrlol69evt2fP1Gykel63bNki/x5oVoqOtmcO1Zc65/6CCy6QmjVrSiPrtR1TpQoJOQqcWh2ybds2eyWOul6om1DHjh2zv9Q1Y/eePZJ8hvGoSgyjK1a0bzSp64W6gapu2Kmq8udb14oq1utb3YgqzNRnXYvWre1rhdP63XCDvPXGGzryXSTohYC/JOiKOrN8zyOjdWRWpWcmS+lruurIW0jQAf9xto9hZsfNy+1QiN/FucnpeeW5hK/KzfWC13f2Xnv9dRl0zz06co66sffDd9/ZBTh9HQl6IXBk9hdy4OXXdeSsWl9/biXoLt7ptl6uu6c9Kymbzv6ayq/AYsWk8phREuTB1xQJOgAAAHzJvn37pPEFF8jBs+R0eaG2C/y1fLlfrMAhQQd8EAk6AAAAfIVKOW+59VZ574MPdI9z1IqF1155xS4m6Q84nwIAAAAAYMz7VmJuIjlXypUrJ9fFxurI95GgAwAAAACM+PXXX43sO//bnQMH+vzZ56cjQQcAAAAAOG7V6tXSq0+fM1a9d0LFihXlvnvv1ZF/IEEHAAAAADhq/oIFcvkVV8j+Awd0j/OGP/igffylPyFBBwAAAAA4Ii0tTZ5/4QW5NjbWSMX2v9WtW1cG3n67jvwHCToAAAAAIF9Upfb169fL1V26yJAHHpCUlBT9iPNU5fYpkyZJaGio7vEfJOgAAAAAgDzbsGGD3DlokFzYrJks+vFH3WtOrx495IrLL9eRfyFBBwAAAACck0OHDsnszz+Xzl26SKMLLpA333pL0tPT9aPmRFesKM9Mn64j/0OCDgAAAADIUVJSkqxbt07eevttib3uOqleq5Zdof37H36wl7e7ITw8XD547z2JiorSPf6HBB0AAAAAIJmZmXLq1Cl7dnzDxo3y1Zw5Muqxx+TyK6+UWnXq2EvYB955p8z55htjR6dlJzAwUB579FFp3bq17vFPJOgAAAAAUMidOHFCWrdpI42aNJEatWvL+Y0by3U9esjESZNk4aJFkpiYKBkZGfq/dl+fXr1k6JAhOvJfJOgAAAAAUMgVKVJEdu7aJdu2b7eXs3tJ2zZt5OWXXpKgoCDd479I0AEAAACgkFNHl7Vu1UpH3tH0wgvl01mzJCIiQvf4NxJ0AAAAAIDUOe883fKGi1u3lrnffCOlSpXSPf6PBB0AAAAAIM2aNdOtgqVm86/p0kW+njNHSpUsqXsLBxJ0AAAAAIBUqVxZtwqOSs6H3H+/fPjBB1IkMlL3Fh4k6AAAAAAAiYmJkWLFiunIfSVKlJB3Z8yQpyZOlJCQEN1buJCgAwAAAACkePHiEh4WpiP3BAYEyGWXXirLly6VXr166d7CiQQdAAAAAGDPWru9Dz26YkV56cUX5asvvrBn8As7EnQAAAAAgK1Bgwa6ZVZkRITcPWiQrFyxQm695ZZCccZ5bpCgAwAAAABsFzZpoltmhIeHyx0DB8rKv/6S6VOnSslCVqX9bEjQAQAAAAC2OnXq6JazqsbEyNjHH5fNGzfK888+K9WqVtWP4HQk6AAAAAAAW3R0tBQtUkRH+VPJ+rv6XX+9fD93rmxcv15GPPywlC9fXj+KMyFBBwAAAADYihYtKuXymESr/2+d886TO++4QxbNny8b1q2Tt958Uzp06MAe81wiQQcAAAAA2MLCwiSmShUdnVlAQIBd5E3tH29/ySVy3733ytyvv5YNa9faRd+ee+YZufjii+395jg3JOgAAAAAgP9q2aKF/We5smWlcePG0rFDB7kuNtZeoj5zxgyZP2+erLeS8V3x8TLvu+/k6cmT5dJOnezl68yU5w8JOgAAAADgv0Y9+qiknToluxISZNmSJfLd3Lny0Qcf2EXe+vTuLW3btLH3qoeGhur/B5xCgg4AAAAA+C8S74JDgg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHuAjCXqA/b+zyTx1SrcA/5aZfFK3chDE/TcAAADAl/jECD4wNESCihXTUfaSVq3RLcC/nVy2XLeyFxJVRrcAAAAA+AKfmWILv6CxbmXv6NdzdQvwX1lpaXLsh/k6yl5o5WjdAgAAAOALfCZBjzy/oW5l78S8BZJ+6JCOAP907JdfJX3PPh1lL7JFM90CAAAA4At8JkEvenEr61+b80b0jKNHZfdTT4tkZuoewL9kJCfL3vGTRLKydM+ZBZUvJ+HVqukIAAAAgC/wmQQ9rFpVCa1eXUfZO/rp55L4yWc6AvxHVnq67Bo5WlI3b9U92SveqYMEBPrM2xsAAACAxWdG8IGhoVKqV3cd5SAjQ/aOfFwOvPOuZFltwB9kJCVJ/IMPy9Ev5uieHFiJeanePXQAAAAAwFf41BRbVN/eElSqpI6yp2Ya9z4+Trbffpec2rqNJe/wWeq1fOynX2TztT3lmErOz7K0XYlscZEUyUXNBgAAAADeEpBl0W3HqeRiU5dYSd0Yp3uyFzVooEQPG6qj7O1/5XXZN3GKjs4uIDhYIpo1laLt20lErRoSVLasfgTwqKxMSYvfJSc3bpTj3/8gKZs26wfOLiA0RGp8+oFENsw5QVerS+I6x0rKxk26J3slu3WVKtMm6wgAAACAKT6XoGempsnm2J6Ssm6D7gHwt1I3XS+Vxzymo+yRoAMAAADe43NVpAJDQ6TK1EkSWKSI7gGghNWvK9EjhusIAAAAgK8xm6AHBFj/y/lotP9KT9eNs4uoc55Umj5JAkJCdA9QuAVXKC/VXn9JAsPDdc9ZqHUzuVw8o7aJAAAAADDPeIIeGJm7me7cHB11upKdOkrF8WPsPbdAYRYUVVqqvf2ahFasqHvOListVTKOH9dRzoKrV9UtAAAAACYZTdDVOczBuai6rpzasUO3cslK/qN6XCdVXnlBAosX051A4RJap7bU+OR9e1XJuUg/dFjS9x/QUc5CKlTQLQAAAAAmGd+DHnZ+fd3KWdqWbZKyc6eOcq9E+3ZS68tPJKJ5U90D+D+17Lxk315S67OPJLxaNd2be8cX/y6SkaGjHAQESFiVyjoAAAAAYJLxBD3ivNzP7B3++DPdOjdhVatKzfffkehJ4ySkahXdC/ihoCCJaHahVP/4XakybowERUbqB3JPVXA//PEnOspZYES4hFU/9xsAAAAAAM6d0WPWlLTEg7KhRVuRzEzdk72QypXkvO/nWElBhO45d5kpKXLsx5/l4DvvyqnVayXzWO722QKepbaKRJWWyNYtpcyAmyWyXj0JsBL1vEpatVq2de9rH4N4NqE1qkudH76xZ9IBAAAAmGU8QVc2XdtDUlat0VHOyg65Vyrcd7eO8if9yBE5uXGTJC9fISmbNkv6sWOSlZqmHwW8KygyQoKKF5fwCxpLkSaNJaxaNbsvv1RSvqXvTXJy2XLdk7PSt/WXSo+O0BEAAAAAk1xJ0Pe9/Jrsf+ppHeUsMDJSqn/ynj1LCMBZB955T/Y+/mTujlgLCpSaX34qkfV5LwIAAABuML4HXSnZ+apcL8nNTE6W+Dvvy1PBOADZO7rwR9k3bmKuzz8Pq1VTIs6rrSMAAAAAprmSoKsq0EWvvkJHZ5cWnyBb+9woJzdv0T0A8sxKyI98+70k3HXvOW3xiLrlJrtaPAAAAAB3uJKgK+XuvP2cBvvpu/bI1tjecujzL3NVzArA/8o4cUJ2TZgkCfcMkayUVN17diHVqkqp2Gt1BAAAAMANriXokfXqSvHYrjrKnUyVXAx9SLbccLOcWLqMRB3IpcxTp+Tgp7Ml7oqucui1t3J35vnfAgKk3H2DJDA0VHcAAAAAcIMrReL+lnYgUeI6d5MM689zZiUNoTVrSNF2F0uRC5rY7aASJfSDQCGXlSnp+w/IqU1xkrRkqZz4dbFkWHFeFLHeY9Xfek0CAl27fwcAAADA4mqCrhz5YYG9F1bSz2FGLzuczQz8PwfeyoElikutObMlrHIl3QMAAADALa4n6CqJ2DPpaUl8+XXdAcALAsJCJebVF6V4uza6BwAAAICb3F/DGhAgFR4cIiWuowAV4BlBgVJh9EiScwAAAKAAFcgmU3UmeuVxY6TopR10D4ACExgo5R4YLGX69NIdAAAAAAqC+0vcT5OVmio7HxsrRz76RPcAcJNa1l5xzCiJ6t1T9wAAAAAoKAWaoNusb5/43geyb8IUyUxO1p0ATAuJqSyVp0yUos0u0j0AAAAAClLBJ+jaqe3bZeeDI+Tk8hVW0q47ATguIDRUSlzbRaIfe0SCihbVvQAAAAAKmmcSdCUrPV2OfP+D7Js8TdJ2xNuz6wCcERASLBFNGttL2iPr1rE6OKYQAAAA8BJPJeh/y0xJkeO/LpbEN96Wk38ssxN3AHlgJeGBRSKl2GWdpMytN0lk/fp2UTgAAAAA3uPJBP10qfv3y/FFP0nSb7/LyY2bJG3LNslKS9OPAvi3ACshD6tVUyIbny9F27aRoq1aSFCRIvpRAAAAAF7l+QT9H6x/qppNTzt8RDJOHJf0g4ckKzNTPwgUXoHhYRJUvIQElywpwSWKSwCz5AAAAIDP8a0EHQAAAAAAP8U0GwAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAFTuT/AEi4PhsWDpChAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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" + }, + "692db549-7ae5-44d5-a1e5-dd20a493b723": { + "name": "HID Crescendo Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a": { + "name": "Excelsecu eSecu FIDO2 PRO Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d": { + "name": "Feitian iePass FIDO Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "aeb6569c-f8fb-4950-ac60-24ca2bbe2e52": { + "name": "HID Crescendo C2300", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c": { + "name": "eWBM eFA320 FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAExCAYAAADvDYgqAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAFicSURBVHhe7d0HeBXF2sDxN73QCTVA6FIFFKkCUuyAEumKYkFUbICCIiKCUgQE7L0gdlQsKCpSrIggSC+hJnRCJ4H0b2fveD/0khCSnc2ek//vuXmYd46XkJNz9sy7M/NOQJZFAAAAAABAgQrUfwIAAAAAgAJEgg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeEBAlkW3PSszNVXSDyTKqa1b5dSadZK6e4+kHz9m94n3//mAcQEhoRJcupQER0VJWJVKEt6gvoRXryZBpUpJQCD34QAAAABf4NkEPSsjQ05t3iKHPvpEjv+wQNL37ZOs1DT9KICzCYyMlNAa1aTENZ2lZJfOElqhvPWOD9CPAgAAAPAazyXoKjE/Mvc7SXxzhpxasVL3AsiPgNAQKdqxvZS9Y4AUadJY9wIAAADwEk8l6Md/+132jHtKUtat1z0AnFa869VSYdgQCatSRfcAAAAA8AJPJOgZJ07InolT5PAHH4tkZupeAKYEhIdLhVEjJKpXdwkIDta9AAAAAApSgSfop7ZslR0DB0nq1u26B4ArAgKk2BWXSpXJEySoaFHdCQAAAKCgFGiCfuLP5RI/YJBkHDmiewC4LbxRQ6n25isSEhWlewAAAAAUhAJL0E8sXSY7brlDMpOSdA+AghJaq4bU/OhdCS5dWvcAAAAAcFuBHJCslrXH33kvyTngEambt8r2gYMkg/ckAAAAUGBcT9DTjx6T7bfdIRmHDuseAF5w8s+/ZOcjj0kWhRoBAACAAuHqEnd1xnn8Aw/JsS/m6J5zFxgZIUGlSklIjeoSVKK47gUKOettnL5vv6TtiJeMI0clKy1NP3DuKk4YK2X69NIRAAAAALe4mqAfXbjILgp3zkepBQZK+PkNJKp/PynWuqUElykjAUFB+kEAf8tMTZXUXbvk6Lfz5NDM9yV9z179SO4Fliwh5/3wDUXjAAAAAJe5lqBnJCVL3FXXSFrCTt2TO6E1q0vFUSOkeNs2dqIOIHcyT56Ugx98LPufeV4yjx3XvblToltXiZk6ybpCBOgeAAAAAKa5lvEemfP1uSXnVmJQomes1J4zW4pf0o7kHDhHgRERUvbW/lLLeg+po9TOxbFvv5dT8Qk6AgAAAOAGV7Jetex2//Mv6SgXrGQ8atBAiXlqvASGh+tOAHkRVqWyfYRa5MUtdc/ZZZ1Kkf3PvqAjAAAAAG5wJUE/sXiJpO/ao6OzCAiQ0jf3k+ih97O8FnCIutFV7dUXz2km/cSCRZJ+5KiOAAAAAJjmyh50Vbn96Gdf6ChnKoGo+fH7EhgWqnvyyfrxstLTJf3ECck4flyyUvNe3RpwizqtILhYMXuZul0Q0aGbVae275DNV14jWSkpuidnlZ6ZIqWv6aIjAAAAACYZT9BVcryueRvJPHxE9+QgOFiqz3pPijZprDvyLnndejk2f6Ek/bpYUrZslYzEg/oRwHcEx1SRiNq1pGiHdlKsY3sJq1hRP5J3+156VfZPmqqjnBW9rKNUf/VFHQEAAAAwyXiCnrxmrWzp2l1HOSva8RKp/vrLeZ4tzDyVIke+/U4SX3tTUtZt0L2AnwgMlKKd2kuZW/tLsRbN8/w+ST96VDZ1vFIyDh3WPdkLKl5c6v7xswSGhekeAAAAAKYY34Oe/NdK3Tq70n165S3pyMqS44t/l7jO3WTXkOEk5/BPmZlyYt4C2X7DLbJt4CBJyWOV9eASJaTEtV11lDN1VFvqzl06AgAAAGCS8QT95PrcJcsBkRFS7JK2Oso9VSF+98TJsuOmAZK6dZvuBfyYStR/WCibu14nh+d8Y8fnqkSXq3QrZ1lpaZLC+woAAABwhdkEPStL0rZu10HOwhvUl8DQcysMp4q+bb/jHjn46pv2XnegMMk8dlx2Dh4me6Y+Y7/XzkVEzRoSWLSojnKWsieXJzAAAAAAyBejCbra3p6RnKyjnKmzms+FSs633TpQkhb9pHuAQigjQxJfeMVeRXIuSXpARIQEl43SUc7Sd5OgAwAAAG4wO4OemSmZuTzOKahCed06O7XsNn7YCDm5bIXuAQo3tYrkwFszdHR26ui2gNDcFX7L2LdftwAAAACYZHwPugn7X3tTTnz3g44AKPsmTZOkFX/pCAAAAICvMXrMmtoXvqlLrKRujNM92YsaNFCihw3VUfZOboqTLV2us2fRcy0w0N5vG1wmSgKjSulOwKOsd2TGzl2SceKEZJ5I0p25E1qrhtT+8lMJjIjQPWeWlZEhcZ1jJWXjJt2TvZLdukqVaZN1BAAAAMAU30rQrX/qttvukBMLc7nv3D43uoOUHXCzhNerK8HFiukHAI/LzJS0w4flxO9/yIHnX7ISaes9lJu3akCAlB8xTMrdfqvuODMSdAAAAMB7fGqJe9Kq1blOzkNiqkj1j2ZK9VdfkKLNm5Gcw7cEBkpIVJSU6nyV1J4zWyo+OVoCwsP1gzmwkvjEl1+TjKRzm3kHAAAAUPB8J0G3Eo8Dr76hg5yFn99Aas7+SIpe1FT3AL5LFXQrc30f+4ZTUMkSujd7GYcOyxF1PjoAAAAAn+IzCXr6kaOS9MtvOspecMXyUu31lyWkdGndA/iHIo3Ol8rPTbVe5EG6J3tHPvtCtwAAAAD4Cp9J0JNWrpLMY8d1lI3AQKk4drSElCurOwD/UrzNxVLqhj46yl7ynysk4/gJHQEAAADwBb6ToP/2u25lL7xeHSnR4RIdAf6p7IBbJSA4WEfZyMiQpJUrdQAAAADAF/hMgp68bp1uZa9El6vt/bqAPwurXEkiWjXXUfZOrV6rWwAAAAB8gU8k6FkZmZK2abOOslfssk66Bfi3Ym3b6Fb2Uvfv1y0AAAAAvsBHEvR0O0k/m7AKFXQL8G+h1avpVvYyT3DUGgAAAOBLfGaJe64E6D8Bf8drHQAAAPA7/pWgAwAAAADgo0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPCMiy6LbjstLTZVOXWEndGKd7shc1aKBEDxuqo3/KTE2VDa3aS8ahQ7rnzBqsXS6BkZE6Mic1PkFOrd+gI/iz0JgYCa9XR0fecWT+AkkYMEhHZ1aiR6zETJ6go3/KysiQuM6xkrJxk+7JXsluXaXKtMk6AgAAAGAKCXoeHJz5vux+bKyO4M+i+veT6Mcf1ZF3kKADAAAA/ocl7gAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACABwRkWXTbcVnp6bKpS6ykbozTPdmLGjRQoocN1dE/ZaamyoZW7SXj0CHdc2YN1i6XwMhIHZlzcvUaOf7jzzryvuQ/V8jxRT/pyFnlB98rEuS/93kiGp0vxdq10ZF3HJm/QBIGDNLRmZXoESsxkyfo6J+yMjIkrnOspGzcpHuyV7JbV6kybbKOAAAAAJhCgl4IJL45Q/Y8ceZELb8axq2RgOBgHcEt/pygZ6WlSVamsctS4RMgEhgSYv1pNQAA/0ONM8WFj52A4CAJCArSUcFz9fOWzyIg10jQCwESdP/jzwn6tqHD5NSKlTpCfgWVKC61Pn5fAkNDdQ8A4HRx3ftI+lnGmE4oP/wBKX3VFToqWBlJSbLlxlsk4/AR3WNWZItmEjP+CQkIZHctcDYk6IUACbr/8ecEPa7fzXJy8RIdIb9Ca1SXuvO+0REA4N/WtWwn6QcO6Mic6EnjpUz3WB0VoMxM2T50mBz7yp3PhuAK5aX27I8lpFw53QMgJ9zGAgA/Flarpm4BACCS+NEs15LzgLAwqTJ1Esk5cA5I0AHAj4WWL69bAIDCLnndetkz7ikdmVduyL1SrEVzHQHIDRJ0APBjYY0a6hYAoDDLOH5c4gc/KFknT+oes4pdcZmUu+0WHQHILRJ0APBj4TVr6BYAoNDKypLdk56W1C1bdYdZIZWipcr4sRSFA/KAdw0A+KugIAmrUEEHAIDC6vA338rhDz7WkVmBkRES88IzElyypO4BcC5I0AHATwWVKiWBxYrqCABQGKXEx8uukaPtWXTjgoKkwqhHpMj5bK8C8ooEHQD8VFDRIhIYFqYjADArMzNTTp48KYcOHZKt27bJ0qVLJTU1VT+KgpB5KkV2DH5QMo8f1z1mqaNZy/TsriMAeUGCDgB+KqRyJQkICtIRAOSNSrzT0tIkOTlZEhMTZfPmzbJ48WJ57/33Zdz48TJ4yBC5NjZWatetK+fVqyd1rK96DRpI67Zt5bhLiSHOQO07f2qynFq5WneYFd6ooVQeO1okIED3AMgLEnQA8FOhVWN0CwByphLwAwcOyNq1a2X27Nny4ksvyWOjR0u/m26Si9u1k6bNmtlJd6WYGKnXsKG069BBbr71Vnl87Fh5wfpvv5k7V+Lj42Xv3r1y5OhRO6lHwTq66Cc5/P5HOjIrsGhRiZk+RQLDw3UPgLwiQQcAPxVKgTgAOTh27JhcevnlUrd+fYksVkyiq1SRJk2bSq++feX+IUNkwlNPyUcffyzLli2T9Rs2yO49e0i8fUTq7j2yc/gIyUpP1z0GBQRI9JOPS3jVqroDQH6QoAOAnwq/oJFuAcD/UvvDF//+u2zZ6s7RW3BHpvV7jX/oEck4dFj3GGQl51EDbpbSXTvrDgD5RYIOAH4qvEoV3QIAFBb7X31dkn/7XUdmRV7UVCoOHawjAE4gQQcAPxQQESEhZcrqCABQGBxfslT2P/eSjswKrlhBqj43VQJDQ3UPACeQoAOAHwouX04CQoJ1BADwd+lHjkjCsIeshvl95wGhIVJ58gQJKcuNYMBpJOgA4IdCSpaUgEAu8QBQGKhicPHDH5H0XXt0j1ll7rpDirdqqSMATmL0BgB+KKRGNc6iBYBC4sDb78iJ+Qt1ZFbR9u2k4r2DdATAaSToADwlpFxZCa1S2bWvkIoV3Ulkre8RUin6jP8GE18R9evrbwwA8GdJK1fJvqnP6MisEOvzJWbKRG4AAwYFZFl023Fquc2mLrGSujFO92QvatBAiR42VEf/pI6L2NCqvWQcOqR7zqzB2uUSGBmpI/wt8c0ZsueJCTpyVsO4NRIQzD5Xtx2Zv0ASBuR897pEj1iJmXzm33tWRobEdY6VlI2bdE/2SnbrKlWmTdaR/zkVHy9xl3U2flZsYJEiUmfBdxJSJkr3AEDBSkxMlKo1atjHrZmyd9cuiYry9nVvXct2kn7ggI7MiZ40Xsp0j9WRM9KPHZO4bj0lbUe87jEnMCJCqn/wjhQ5v6HuAWACM+gAAACAD9r1xHhXknMJDJTyjz5Ecg64gAQdAAAA8DEHP50tRz/7QkdmlejaWcr06qkjACaRoAMAAAA+5GTcZtkzZpyOzAqrc55UGTeGk0EAl/BOg09R9Qi29rtZ1l3QwvhX3LU9JONEkv7OAAAABS/jxAmJv/8ByUwyP0YJLFZMYp6fZu8/B+AOEnT4jqws2Tf9eUn69XfJOHLU6Fdm8kmpOHqkBBUtor85AKCwyMzMlPT09DN+ZWRkWB9HxurrAjnKsl6buydMylWR13wLCpToMaMkokYN3VF4qfd8TtcF9RjgFKq4FwL+UsX92M+/yI5b7xTrSqh7zCl7/91SYfC9OvIeqrg7hyruvi8lJUXWrl0rf61cKfv375cDiYn6EZGw0FApWbKklC1bVmrXri316tb1fEVpuCcpKUm2bt0qq1avlj179si27dtl48aNcurUKTl58uQZB90RERESEhIipUqVkgb160ulSpWkatWqdrty5coS7EMnm1DF/T98qYr7ke++l/h7hqi7SLrHnNL9+0nlUY8UuiPV0tLSJN4aGyxfsUISEhIkLi5ONllf6rqQnJys/6v/p97zYWFhUrx4cWnYoIF9HahWrZo0btTIvj740jUB3kCCXgj4Q4KeunuPbLZeSxmHj+gecyJbt5QaM9/09F4rEnTnkKD7pqNHj8rcb7+VDz78UH786Sc70cqtOuedJ48/9pj06NFD96AwUMn2tm3bZPHvv8uiH3+Uv/76S1auWqUfdUZ4eLg0b9ZMLrjgAmnVsqW0bNHCHqB7FQn6f/hKgp6SsFPirukumceO6R5zIi5sIjVnvi2B4WG6x3+p17+6wfvLL7/I/AUL5PclS+SYQ89xpJWXtG3TRtpfcom0sK4HzS66yL5OADkhQS8EfD1Bz0xJka3X95eTy//SPeYElS0jtb+eLSFly+oebyJBdw4Jeu78biU169av15EzLrIGKo3OP19HuXPAGkS//OqrMuXpp884k5Fbsz76SLpde62Ozt2sTz6R48eP68h5V15xhURHR+vIGZ999pkcOXpUR867xBqA1vTYUli1HH3Tpk3y2ezZ8smnn8r6DRvsPrcEBQXZN4R69ewpV115pTRo0MCeaTNp1apVsuzPP3WUsxMnTshDI0bYS3RNeXryZClatKiO8q58+fLS+eqrdeQsX0jQs9LSZHO/m+XksuW6x5zgcmWl1uxZElqhvO7xP+o1r27QfWh9Frz73nty8OBB41tXAgICpFixYtKje3fpd/310rx5c+PXgzNRNys//+ILOXLE7KRX6dKl8/U5m1fq53tn5swzroByirrJ0rtXL/sabwIJeiHg0wm69fLc88zzkvjsC1Zb9xkSYF0kq779mhRr2Vz3eBcJunNI0HPn/iFD5MWXXtKRM+675x55esoUHeVMDaZmzZolQx98UBKtgVR+qOWGf/7xh9SvX1/3nBv1sdmoSRPZsHGj7nHet998I506dtSRM5o2a2Yv5Tblnbfflr59+uioYKkVFd9//71Msl5fahCulqwWNDWQU0tfB9x6q/08xcTE2AN2p02dNs1Ouv2NSmo+sBIpEzyfoFvXnF0TJsvBN97SHeaoMV3V11+S4m3b6B7/om7sfj9vnjw1aZKs+OsvV2/YnU6996tXqyaD77/fTvRUMuum2wcOlLffeUdHZqibEbsTElxfMaC2LdU//3yjv9sO7dvLd3PnGrmGKxSJg6cd+22xHHzhFePJufUOkzKDBvpEcg74i4SdO3UrZ4cPH5brb7hBbr7ttnwn54qazVOJEvyPWqr63vvv2zcjevXta88keyE5V9RgcceOHTJq9Gj7Bs+1sbGydNmyAksQfE3rVq10q/BRNXgOzjCbTP2tzF23+2Vyrm7yfvnll9KkaVPp2bu3fW0oyPeeutG7dds2uW/wYKnXsKE8PXVqvlaFnatevXrpljlqlZmqD+O2JUuWGP/d9rFeQ6aSc4UEHZ6VdiBRdj3wsPGZTSWyWVMpf9dAHQFwwxrrg/tsi7jUnuG27dvL7C++cGy5WpkyZew7+/Af6nX0888/S5t27eTmW2+VLVu36ke8KfnkSbuGgvr3XnHVVfYWEuSsRiGtJJ66b78kDH/EyjDNJ5NF2l4sFe69W0f+Q23PurpLF+luJaXqM8VrDh06JA8/8oh9Y/GLL7886+eiEy6xrj2q0KVpc7/7TrfcM3/hQt0yQ60IuC42f8Uez4YEHZ6k9lolPPiQpFsfTKapvVYxz0+XgJAQ3QPADceOHrUrZWdHVc7teOmldlVtJ114wQVG73zDXWof9dAHHpDLrrzSXrLqS9RNJ1XkUN2E6t6zp2zc5MLRWT5I7dNVVfILG7XFM+HhRyTjwP+fTGFKcMUKEjN5ogQY2lNbENSKmslPPy0tWrWShYsW6V7v2rxliz273++mm+x6KyaFhoZKd8NJprJ48WLdcoe6pqoioCapmxvqdBiTSNDhPVlZsu+lVyXpp191hzkqKa80aZyElC2jewC45djx4/by9TPZvXu3XG4lXDt37dI9zimMA31/pWbD2nfsKM+/+KLPLxX/8quv5KLmzWXsE0/YNx3w/0KCg6VChQo6Kjz2v/G2O2OhiAip+vx0vxoLqWMTu15zjTwycqR9PJqvULPnH8+aZc+m/7F0qe4147rrrjN+s1otcTd5SsS/qaMy1VYik27s10+3zCFBh+cc/32JJD7/so7MihpwixS/pJ2OALhJzZ6rs2b/TRX46tWnj5HkXFHVxuH7llqDV7VE3Omj0gqSSiSeGDdOWl58saxYsUL3om7duoXuaKrjfyyVA9Of05FBgYFSYfhQKdKkse7wfStXrrSvDQt8YNY8O3v27rVvUs98911jS95VXQd1DJxJu3bvNp4wn27evHm6ZUZERIR9yoppJOjwlLT9+yXh/gftJe6mRTS/SCoMvU9HAAqCutt9OrU8bcgDD8iSP/7QPc4KCw0ttHtZ/Yk6r/iKq6+W/S5U3i4IaltH3379XJ158rKaNWvqVuGQfuy47Bz+iCs1eIpfeZmU6Xe9jnyfWlLdvlMniU9I0D2+S92sHnjnnTJt+nQjSXqRIkWk2zXX6Micb13ah66eo3k//KAjM9TpKiVKlNCROSTo8Az1QaQKobix1yooqrTETJ1k/Ax3ADn77V/709TxN2rGwJSyZctKKcN7x2DW+vXr7RUWJs+h94LJTz1l7xOFSLOLLtIt/6eOQU0YOUrSEnJ3ykV+hNauKVUmjpOAQP9IB3788Ue5qksXv9oioqrPjxg5Up559lnd4yxVjdy0xS4VwTyVkiJ/GLq5/7cBt92mW2aRoMMz9r/+piT9+IuOzLH3nU8eL6GVonUPgIJy+hL3o0eP2kfOqAGJKeXLl7cLTsE3qWrHsT16yIFE8zdyC9KdAwdKVyvRwH80bdpUt/xf4rvvy/FvzM84BhaJlJjpUySoSBHd49vUqqtu3bvbs87+Rq0sG/bQQ/Lee+/pHue0bNlSihcvriMz1LFnKVbybNrmuDjZu2+fjpynzqpv17atjswiQYcnnPhjqeyfPF1HZpUecLOU6NBeRwAKkpoN/dsbb75p/AicphdeSAV3H6WWL6qCT1u2bNE9/kkVMZwwfryOoPaeV6taVUf+LXnNWtk7eaqODAoMkIpjRklk3bq6w7dt375duvfo4ffFFe+8+27Ht3+pauRXX3WVjszYt3+/7Le+TPtm7lzdMkMtb3friFYSdBS49EOHJGHIcHWLUPeYE9mimVQcwr5zwCvUHmK1VDkxMVHGjB2re81RxabgmxYtWiRvzZihI/+k9oS+/eabUrRoUd2DkiVKSFRUlI78V/qxY7Jj8IOSddJwxfGAACnd73qJ6nat7vBtasa8d9++dhLo71QRyRv69bNXEjnJdFVyNXv+7+1sTlOrDEzudVc39u+4/XYdmUeCjgJln3c+bISk796je8wJKlVKqkyfzHnngIeo5ez79u2T995/X5JzOBPdKer8Uvge9ToZNXq0PQjzV2oAOOKhh6RJkya6B0r5ChXsysl+LStLdj05QdK2/bNopgnh9etJ9EMP2om6Pxj75JOy3MUTD4KCguxio2plh/pSW6ZCrHGlWyuzdsTHy52DBjl6LWzerJmUKWP2iL1vv/1Wt8w4euyYrDttRZ7ToitWlGbW8+QWEnQUqP1vzZATC37UkUHWhTN6/BgJLYTnqAJepqpUr9+wQV562fzRimogVa1aNR3Bl6iq7aYq+3uFOvJo6JAhOsLfGjdqpFv+69CXc+ToZ1/oyJygMlFS9aXnJNBPjqz7+eefZeq0aToyRxVrvOLyy+WF556TX3/6SbZt2SJ7d+2yv/bs3Ckb1q6Vb+bMsW+w1a9XT/+/zPnK+l5OzharquQdDB8/+rN1Dc/IyNCR89TJF06vLDhd+/btjR9JdzoSdBSYE38skwNTntGRQVZyXvq2/lLyyst1BwAvefOtt2TL1q06Mqdq1aqufsDCGWrv+bPPP68j86pUqSI333STPD15ssyzBsEb162TrXFxsm/3btm0fr2sW71a5n//vTz3zDMycsQIuaZrV6lXt64E5+NUkKjSpeWdGTPsmTj8U+3atXXLP53avkN2jx5rz6KbFBASLJUnPCFhflIgV+03HzBwoI7MULPivXr2tN/zc778UgbefrtdsFCdBqK2o6gvtSc5JiZGLu3UScaOGSPLly2T2Z9+Kuc3bKj/FuepFUX3Dx7s2J579XPecL3Zo/ZUYc+9e/fqyHnfWddkk9xc3q6QoKNApCUelIT7H3DnvPMLm0jFB5mVALxqztdf65ZZlaKj85VEoWAcPHhQfvr5Zx2ZU716dXlv5kw7IX/t1VflvnvvlfaXXGKfm6+SdlXBV/03KmFs166d3HnHHfL46NHy6axZsuLPP2VXfLx8/OGH9kC3SuXK+m/NnSmTJkmM9T3wv/x5W0pGcrLEW2OhzOPmi5tF3XaLlOjYQUe+b9KUKbLVYFHR4lbiPXPGDHn3nXfsm7u5pZbAd+ncWRb/+qud0JuyfccOef6FF3SUf5dY1zpVMM6UZOu1/qd1nTRB3cT94gtzK1DU7/8il496JEGH67IyM2XX6LGSvtfcUQh/U8u5Yp6dKoEcqwQUem5/wMIZq1evto/gM+ni1q3lj8WL7dmyvMxiq0G5SuBju3Wzi7ytW7NGflq4UHr26HHWI4xu6NtXbrjhBh3ln7pxsNMavOfma9WKFcbPWl/1119n/N65/VL7Y/2RGgvtfmqKnFqzVveYU6RNa6k49H4d+T61D9vJ5PTf1EqrD99/X3r36mXPLueF2lL17PTpcv+99+b57zibqdbf79S1Ua0GuLRjRx2ZMX/BAt1ylqoQb/J0j8svvdT11U0k6HBd4tszXTnjU4KDJHrCExIaXVF3ACjMateqpVvwJaZnz1Vi/cF77zk6e6SKR7Vq1Uref/dde3nsxPHj7RUc/x6oq5n2p59+2tEBvEou1Hn/uflSS3VNK2d9jzN979x+qZsf/ujoDwvk8Acf68ic4HLlJGbyRAnwo+dxivWeUad/mDJ61Ci57LLLdJR36rU7ftw4Y6tADh8+LK++9pqO8kddg9QNSpN++fVX3XLWXytXGi0ya7rK/ZmQoMNVSStXyb4p5gt6KKX69paSnfxnOReA/FGzpPA9JivzKpdbA/GKFc3dyFVJ5gNDh9qz6mrfutqvqqhK0G++/rq9/xyFS8quXbJzxCgRg0WzlICwMIl5fqqElDN/I8YtO3fulLcNHrfYonlze3uLU9QKlReff14iDZ1E8Jp1DVF70p2gbkoUMVinZc3atfZNBactMDQzr6itR82t14TbSNDhmvRDhyXh3qHmz/i0hDeoJ9GPPqxuCeoeAIWZWm4Ycw77COEda9et0y0zqrtU2V/NbN8xcKC9rHzUyJEyePBge98nCpdMfbxs5pEjusecwKJFJMzPTq6Y+e679nngpowZPdrxWiWqbsWtt9yiI2dt275dFi5cqKP8KVq0qHTp0kVHzlNHw6lq7k5S+89NFojr2rVrgaziIUGHK7LS0yXhoUckLWGn7jFHfSBVeX6aBBreVwegYKgjYa6+8koZ/+ST9pE3CdYA5ejhw3LM+jp66JBs37JFfrMGAWr/31133GGfK93m4ovtGUv4nsTERN0yI82FYqWnU3s9Hxs1Sp4YM8bY3lR41/6XX5PkJUt1ZFbGwUOy8/En7f3u/uDkyZPynMG9561atpSOhvZh33P33cb2Mb/l4IoCVTfDpCVLluiWM/bs2SMbN23SkbOCAgPl+r59deQuEnS4IvHDj+XE/EU6MicgOEgqTZ4g4Zx1DPidqKgoefyxx+wq2198/rkMe/BBe+lZhQoV7OWDEdaXmqWsVKmSNLvoIrnrzjvl2WeesYt/fTF7NskQzihu82Z7FsZtvB4Lp+AyUbrljuNzv5NDn3+pI9/2w/z5cuDAAR0575abbzb2vqxmjUsbnX++jpyl6nQkJSXpKH/atW1rf5aaov6tTl5vf7cSfqeW+P9b5cqVpemFF+rIXSToMC55zVrZN26SWoeie8wpqfadX5H/wh4AvEMNl9SxNSuWLZORjzxiJ+rnQg241BJ3+CbTieyChQtlm8HjmoDTRfXsLpHNmurIBdbYa8+TEyTNYGLrllmzZumW89QKq64Gl3erZdLXxcbqyFn79u2TpUudWZVRqlQpu2q5KWofempqqo7yb9Eic5N/3bt3L7AilSToMCrjxAlJGDpcsgzuF/pbWP26Ev3IcDWa0z0AfJ36cLz//vtl1kcfGS3kBe8yfcyWqgZ9ddeukpCQoHsAcwKCg6XSE49LgIvHNmUePSY7R41xZaLElJSUFJnzzTc6cl4z6zpTpkwZHZlxmcHE9+NPPtGt/OvVq5duOe+ElRcsX75cR/mnbrCaoG7Y9L/pJh25jwQdxmRlZMjOkaMlNc7c2YR/CyxWVGJemC6B4eG6B4A/GHTnnTJp4kTHi/bAd1RzobifOkO3WcuW8sGHHzo6uwOcSUTtWlL2vrt15I7j8+bLQR9e6v7rb78ZPVqtk+EzwJW6devqlvNU8TVVhM0J6lg4VSvDFLVVwQm7du82tv+8Zq1acl7t2jpyHwk6zMjKksT3P5RjX5m72/lfgQFScexj7DsH/Eznq66SyZMmGV/iDG9r1KiRbpl18OBB6X/LLdK0eXOZ9ckncvToUf0I4Lxyt/aXsLp1dOSOveMmSuqevTryLV9//bVuOU99xnRo315H5oSHh8v5DRvqyFm7rWT10KFDOsofdTRkG4PHkqrz0J3Yhz537lzdcl732NgCnRggQYcRyes3yL6JU9zZd96zu5S+tquOAPiD0qVLyysvv1xg+7/gHepcYrdu0qhB44YNG+T6fv2kboMGcu/998uyZcvs5bWAk9SKv8qTxkuAi6dLZBw+IgmPjLJXOPoSVQTsx59+0pHz1PFi6ig009R1rGKFCjpy1rFjx+yCl07p37+/bjlv06ZNjqxUMra8PSxMbr75Zh0VDBJ0OC7j+HFJuGewZCWf1D3mhNaqIZVGj2TfOeBH1CDmybFj7bv4QI0a1nU+OlpH7lHHu738yivSqk0badSkiTwwbJhdiMntY9ngv4rUryel+/fTkTuSfvlNDs3+Qke+QS1tX79+vY6cp07/KFmypI7MKmHw+6xZs0a38q/9JZdI8eLFdeSsnbt2yfbt23WUN+qm6dJly3TkrAb16xfIZ87pSNDhrKws2fX4k5K6bYfuMEftO6/68vMSaPA4CADuU/v0buzn7qAV3qUGz7HduumoYGzdtk2efe45ad22rdSoVUv63XSTfDxrll09GcgzNaM6+F4JqRqjO1yQmWlXdU/Zbn6c5hSVzKUavDGm9hqHurSSoVzZsrrlvN8WL9at/FOnpbRs0UJHzpv77be6lTfxCQn5TvKz0+3aawt89R4JOhx1cNancnS2C0VIrDdOxdEjJbxmDd0BwB+o2fOHhw+39+oBf7vn7rs9c1TeXisp/+jjj+WGG2+UKtWqSYtWrWTM2LGy6McfjRaxgn+yl7pPeMLVlYCZx09IwqOjfWapuyqAZlLVGPdukJQrV063nLdnzx7dyr/AwEC5yeCN8vxuWfjhhx90y1mhISFGl/fnFgk6HHNy4ybZM/pJV/adl4i9RkpfV7AzKgCcV7lSJfvuNXC66tWrS4/u3XXkHWrP+vIVK+TJ8ePl8iuvlErWQL9Xnz7ysZXAq8GyE4WQ4P+KtWgupa7vrSN3JC9eIgdmvqcjb1OnLJhUqnRp3fJtcXFxuuWMK664wtjKgpUrV+a5toe6rn5j6Mi9pk2bGqsTcC5I0OGIjKQkib/7fnfOO697nlR+YjT7zgE/1Kd3b3tJM3A6tbJizOjRUqxYMd3jPWrQePLkSZn9+edyw003Sf2GDeWKq66Szz77jJl1nFWFwfdKkOFzuP9t/9Rn5JQPLHVXN8FMenvGDKleq5YrX09Pm6a/q/MOHjokpxwch5coUULatmmjI2eplUjqmLS8UNfTPw29Jq695hr786agkaAj37IyM/+z73zLNt1jTkBEhFR55mnOOwf8kFpaNuC223QE/FPVqlXliTFjPDF4yo0TSUmycNEi6X399VKvQQO7yNyOHTuYVccZhZQuLZWefFytLdY95mUmJcvOh0dKVnq67vEeVZTRyaXbZ6ISvp07d7rypaqtm6LOQVc3CZ2irrU9e/TQkbPU71UV3cyLzZs3y4EDB3TkHPXz3mBdr72ABB35dviLr+Top5/ryCDrjVPxsREScZ75ozAAuK9+/fp2EgZk546BA+Xqq67Ske/Yt3+/XWSuVp060veGG2TFX3/pR4D/V6JjeynWqYOO3JG89E858P6HOvIetQz6FMcc5k5WluOnTJicUf5qzhzdOjfzDO0/v7h1a6nggeXtCgk68kXtO989YpR9UTCteLeuEtW7p44A+JtOnTpx7jlyFBwcLDPeeksuaNJE9/ieTz/7zC4spxJ1dR4w8LcA6/pXeexoCSrlzpFff9s/aaqc3OTs/mWnqPOy87pXubDJyMx0fIa+TJkycvlll+nIWUv++EMy8lCo8Lvvv9ctZ/Xt00e3Ch4JOvIl/t4hkpWSqiNzQuvUtj60HrNn0QH4p+s99OEI71L7Ir/+6itp0rix7vE9apn7J59+Kk2bN7crwCcnJ+tHUNiFlCsrFR59WEfuyDx5UhIefFgyrWTYa1TCefToUR2hIPTp1Uu3nLVv71572f+5OHjwoPy5fLmOnBMREeGpArUk6MiXNJfOO495dqoEFS2qewD4m0rR0VKvXj0dATkrW7aszPvuO7n80kt1j29SBZ1UBfiL27a191UCStQ110jRDu105I5Ta9fJ/ldf15F3qJtZ1G0oWB07dpSQkBAdOeekdf071+0+a9audXSf/d/aXHyx0SPwzhUJOjyv/MMPsu8c8HNNmjQxMgCA/ypZsqR89umnMuT+++0ze32ZGnS2veQSmfvtt7oHhVpggESPGikBLp/9f+DFVyXZStS9JN1Hzmr3Z9HR0XJxq1Y6ctbXX3+tW7mzaNEiIzdsbjR45ntekKDD89JU9U7ungJ+rRrF4ZAHYVYCM+mpp2TWRx9JlcqVda9vSjx4UHr06iXvvf++7kFhFl41RsoPG+Lq1r6slBTZOfIxyXK40Fh+sP3DG/r27atbzjrX5erz58/XLeeobVOm9tnnFQk6PO/gq2/KsZ9/0REAAP90Tdeusuqvv+zZdDXY8lWqINaAgQNl1ief6B4UZmVu6CvhDdzd+nNq9VrZ+8JLOip4xdjemGtqJVFkZKSOnNWhfXv7hqjT1Oqh/fv36yhniYmJsvTPP3XkHHXWe1RUlI68gQQd+RLZoplumZOVmiY7HxwhqbvNnoMJAPBdRa2BvJpN/8sawPXu1cvYQNW09PR0uf2OO2T5ihW6B4VVYGioVJk0QQLC3V3qnvj625K0arWOCpapI778kXqm1EkXJqgjUBs2aKAj56jl6r8tXqyjnC3+/Xf7+ui0/jfdpFveQYKOfKky9SkJKmP+rlPGgUSJH/yAJyuMAgC8o3LlyjJzxgxZuXy53HvPPRJVurR+xHckJSVJ/5tvZnkv7Bo8ZW6/TUfuyFJV3Yc/Yld3L2iqNgn1SXInwOAMupqdv7l/fx0567ffftOtnP3000+65Zzy5crJpZ066cg7SNCRLyHWC7vKM1MkIMTMHbvTnVy6XPY9+4KOAAA4MzXrVq1aNZk6ZYps2rBB3nrjDWnVsqVPzcZt2LhRJkycqCMUWtZrtvydt0torZq6wx2pcZtl74sv66jgqISzSJEiOkJOVBIdGhqqI+d1vvpqCTPw96uZ8dwUfvs1l4n8uVDV29XqK68hQUe+FWvdSqKsDw83JL78uhz71fk3KADAPxUvXlz63XCD/LRokWxct04mjBtnD8pMDDSd9tIrr8i+fft0hMIqMDxcKk94UiQoSPe4Q425Thg4c/pcqH3P4S5Xs/dV5cqWNZqgq2rujRs31pFzVq1aZR85mRN7//myZTpyTu/evXXLWwKyDB4umJWeLpu6xErqxjjdk72oQQMlethQHf2TWta8oVV7yTh0SPecWYO1yyXQR/ecmZT45gzZ88QEHTmrYdwaCQgOtn/X2269Q5J+/lU/Yk5wubJS66vPJMT6s7A6Mn+BJAwYpKMzK9EjVmImn/n3npWRIXGdYyVl4ybdk72S3bpKlWmTdeR/TsXHS9xlne3XsEmBRYpInQXfSYgLW0JMuH/IEHnxJXOFg+6+6y6ZPm2ajrxNfWw2atLEnuE05dtvvpFOHTvqyBlNmzWTVavN7St95+23pW+fPjryNvU7PHbsmHwzd64stBJ3tcRyy9atRvY35tewBx6Q8ePG6chZatBbtUYNuzidKXt37fJcAaZ/W9eynaQfOKAjc6InjZcy3WN1dO52TXhKDr7+to7cEVI1Rs6bM1uCCmh8rd6TdRs0kB07duge56nVNu3attWR7zqvdm15aPhwHZnx0ssvy32DB+vIOQt/+EHatGmjo//1yaefSt8bbtCRM9S551s2bZLw8HDd4x0k6IWAGwm6knbwoMRd3U0y9pv/kIts3VJqvP2aBBTSfUkk6M4hQc8dEvT/R4J+Zr6UoP+bSgLUTLVK2NWXmqlRlYUNDpFyTc1aqZl/E4NIEvT/8JUEPeP4cdl49bWS7nLR3NL9+krlx0fZy+0LwiUdOuS6kFheXHXllfLl55/rCDlR18VqNWtKmsNH8Y146CEZO2aMjv7XgNtvlxkzZ+rIGX1697brlXgRS9zhmBDrA7jylImuLMFKXrxE9r7wshop6x4AAPJGVT6uVKmS3D5ggMz+9FPZsHat/Pzjj3LXHXdI1ZgYY5WRc2Pv3r2yctUqHaEwCypWTCo9aSUxge4O3w9/OEuO/1lwS92bXXSRbpmxes0aycjI0BFyUrZsWWnZooWOnJPTPnR1A3HxkiU6ck6P7t11y3tI0OGo4m0vljJ3ubAf3XoTH3zxVTn+x1LdAQCAM1TRoBbNm8uzzzwj661k/aeFC2XQXXcVSEX4zMxM+frrr3WEwk6Ns0pc01lH7lArzHYOf0QykgrmVIHatWvrlhknTpywt7zg7FShzWu6dtWRc9SKtJSUFB390549e2TLli06ckb58uXtlRNeRYIOx5W/Z5BENDd7t1PJSkuTnfc9IGkuLKkHABRO6oinZs2ayTPTpsnWzZvlpRdekNq1aulH3bHkjz90C4WdOkor+uFhEhTl7s2itB3xsnvi5AJZudjCwIzt6VRyvinu7Ntx8R/XXnut4ydiqJVC27dv19E/qTohTq9w6NK5s9GCevlFgg7HBYaFSswzUySobBndY066lZwnDHtYstJZmgQAMEsd+TTgtttk5YoV8uTYsRIREaEfMUvtiWcJLv4WUrasRI8e6fpS9yMffyLHfjO3Fzw71atVM3rUmlql8sMPP+gIZ6N+H82bNdORc+Zks1LI6RVE6ji63j176sibSNBhRGiFClL56Yn/LSBnUtJPv8q+51/UEQAAZqlZdVUt+fNPP5UiLhSnVUXsfHUJrhcr4/uDkldeIcUudbaQ5NnYS92HjZD0w4d1jzvUlpP69erpyAyVHHqhKKSvMFEQ9EyFAJOSkhzff17RylFat26tI28iQYcxxdtcLKXvuE1HZiWq/ei/swQQAOCeDh06yPBhw3RkjprhU/tknaaK3zm9VPXf2NtrRkBQkFR6/FEJLFFc97gjfd9+2TV+kqtL3YOsn/XSTp10ZMZfK1fKtm3bdISzueLyyx0vnrl8xYr/OQ9dHX+pKsc7qVu3bvb5+l5Ggg5zrA/9ivffIxEtnV8G82/2fvQHHnL9ri4AmKASMiepmSGnj8XBfwom3XbbbcaXuqvf378Hrk5Qg1TTCbrJI9wKu9Dy5aX8g0N05J6jn38pRxf9qCN3XHHFFbplhlrp8ZZHj9zyoho1akjDBg105Ax11OWu3bt19B+LFy92dGWDutnTz+Hz1E0gQYdR6pzyKpMnSlCpkrrHHHUuaMJDI42fZw0Aph0/fly3nPHJp5/K+g0bdAQnlS5Vyt6T6YtMJ+fKZoerL+OfyvTqKRFNL9CRSzIzZddjYyX96FHdYZ46VUEd8WXSjHfesZdU4+zUPu4b+/XTkTPUTZLffvtNR/8xZ84c3XJG1apVpdH55+vIu0jQYVxY5UpSaepT9nIs007MWyD733hbRwDgm5xc0hcfHy/3DR6sI/9y8OBBOXCgYE/yUANVtSfdNDXz47Tw8HAJNJykq2WrMCcgOEgqj39CAiLCdY871KTIzkdH28m6G9Ry6l6GC3up47zGPvGEjgqe0yupnKaOKXO6Evrcb7/Vrf/cqP7lXwl7fl17zTWert7+NxJ0uKLEJe2k1K036cisA1Omy/HFzhaUAAA3rXAoqVH7f/vdeKMkJibqHv+hkvOu114rzVq0sCswF+Rg1vRuXJWcFy9uZq9x48aNdcsMNSNG8S2zImrVlHL3DrK3Frrp2Lffy5H5C3Rknqq8bXrVx6uvvSZr163TUcFQ25Heffdduenmmz1dZLFatWpSvXp1HTlDFYr7+2detWqVoysa1M3UW/r315G3kaDDHdYFteKQ+yS8SSPdYc5/qow+LOmHj+geAHCOGiCWKlVKR2aoY7Xym9SkpKTILbfe6ngFXC9QMyvXxsbaz5Pas9jFStT733KL7P7X/kU3qL3hpm+AhAQHS4kSJXTkrMqVKumWGcv+/NM+4xhmle1/o4TVq6Mjl2Rmya6RoyXNpVUszZs3N17N/YSVEKqbmkddXL7/N3XNX7lypVx6+eVyy4AB8vGsWfLsc8959gaXWjnU7/rrdeQMdeN1165ddlsl607+7OfVri21rS9fQIIO1wRGREjMc1Ml0NAswOnSd+35z/noHl8eBMA3mS4KtnrNGlm7dq2Ozt3JkyflZis5/9Lh/XteoKqZ9+7bV5b88f8nd6gzwj/86CNpfOGF8tSkSUYqnmfnp59/Nn5joEmTJsaW0VcynKCr38XjY8bkeaDNMW25ExgeLlUmPGnX/nFTxsFDsvOxsSq71D3mqJUkQ1zYrrPGuvb26tPH8VogOVFJ6W233y4tL774v8eNqffMqNGjZf78+XbsRdfFxjq6/Ubd8FQ39RSnz6bv0qWL45XnTSFBh6vCKleWSlMm6MisE/MXyYF33tMRADjH1Gzm6cZPnJinpEYN9GK7d7cLw/kbNWDue8MNMi+bgduRI0fk0ccekzr168u06dONz2yr/e+Dh5ivon3hhRfqlvNat2qlW+bMfO89eeONN87p9bxp0yYZPHSotGvfnkrwuRTZoL5E3eLOdsLTHZ83Xw598ZWOzFIJYaXoaB2Zs2DhQmnTrp2sW79e95ixdds2GTZ8uNRr0EBmvvvu/9yQUq99tTpo+/btusdb1BL3enXr6sgZ38+bZ2/P+vHnn3WPM26znkdfQYIO15W8rJNE3TlAR2btnzRVklat1hEAOMPp42XORCXYL738cq6TGjXz8N7778tFzZvL/AXu7Qt1i1qyP2DgQPn2u+90T/ZUkb3hDz8sNWvXtgvkLV261PFj5rZZA+bOXbvaA2yT1L5Jk8WxatWqZX8Pk9Rzf89999mJxurVq8+YcKvX70YrKX/nnXfkyquvlkYXXCAvvPiivY1BJS7IhYAAqXD/PRJavarucIl1jdoz7ilJdWErQ7FixeTRkSN1ZJZKzlu2bm2vyjns4DG+aoWTmhVXK4HqN2wo0599Vk7mcIzi/gMH5NrrrnN1ZVBuqZU9vXv10pEzVN0Kdc12sq7IBdb1RB0N5ytI0FEgKgy+T8IamN1HpGRZF8GE+x7gfHQAjlLFcUxTifnQBx6QIUOH2rMnahn3v6k+dXasKijUtFkzueW22yTx4EH9qP9QyZv62T6bPVv35E6y9RmgbnK0bd9e6jZoII89/rgssxI+NTuTl9UJasCoKj1PfOopaXLhhbLir7/0I+ZUrlxZzrcG8aaoGbCSJc0fhZphPXcffPihNG/VSirFxNhJuNqG0cdKUlpdfLFUrlpVLrCe09sGDrRvMJ3+ele/N7U3FWenlrpXGjdWrQfXPe7IOHRIdo4cLVkZ5rcWXn/99UbfE6dTybRalVO3fn15cPhwWb58+Tknyuq1rFbzqO0walVInXr15KouXezr2Zmu62eybt06ueOuuzxZ2V0l6E7e5NuwcaN89vnnebpGZ0dVbzd9I9JJAdYP79xP/y+qWNemLrGSujFO92QvatBAiR42VEf/lJmaKhtatbff/DlpsHa5BEZG6gh/S3xzhux5wsyy8oZxayQgj/s5Tm3bLlu6XieZScm6x5yiV14m1Z6f7spRb25QVVMTBgzS0ZmV6BErMZPP/HvPsj4Q4jrHSsrGTboneyW7dZUq0ybryP+cio+XuMs6Gz8/P7BIEamz4DsJKROle3zL/UOGyIsvvaQj591tDTymT5umI+/7a+VKad6ypaMDiJyo47BqWImUOgv472RKLef+fckS2blrl6t7JbPzzttvS98+fXTkHDVzrhI5p5bsqyJ/ZaKi7JssHTt0kPPPP98uPFW6dGm7UrraT6m+1MBZfamZM1WI7pdffpHvvv9e/szDAD0/Hn3kERltJQgmXdutm3xz2vFGXjT4vvtk8qRJOnLWupbtJN2FQmfRk8ZLme6xOjLIui4lPDZGDr//ke5wifXeqvTUOIly4Wf82Xo/qmJqBZGwli9f3i441rJFC6lQsaJ9bVbXjrDQUEm3rhlqmbq6qaqKI8bFxcmvixfLgf375eixY/pvyLunJkyQoS5sqzkX6nfQuk0b+9rolL+vwU5Q1/y1q1b5TIE4hRl0FJjw6tUk2rqQiwt3tE58O08OzJipIwDIHzU4i7CSZreoGWS13PKtGTNk2jPP2F+qvX7DBk8k56aoga7a4+3kfnp1U+VAYqK9dPqpyZOl3003yYXNmkm1mjWldNmyUrVGDXvZaYyVwKu45nnn2fugH3n0Ufnxp59cTc7VTYN777lHR+b0MXBjxWkvvfKKbLKSHeSCWuo++F4JKldWd7jEem/tnThZUvft0x3mXNy6dYEdmaVWLakbBJOffloeePBBu+ZHp8sukzaXXCLtO3a0bxyo7Thq5n3GzJmyefNmR5JzZfTjj9v7471EzUx369ZNR85wKjlXLmjSxKeSc4UEHQWq1FVXSKm+zu5dyc7+ydMleW3Bnm0JwD9ERkZKhw4ddAQTVHJ+/+DB8vqbb+oed6iVCfEJCY4NqPNj0J132km6aZd26iTFixXTkTeplRQPPfywa6tWfF1IVJRUGjPKlUmQ02UcOiwJwx8xvyrN+rmenjLF8QJlXnfKeh/c1L+/7NixQ/d4w5WXX27PVHvRDQ4fBecGEnQULOsCGz1qhISfb77gUtapU5Jw71DJOO69IhsAfE/PHj10C05TSyYfHDZMXn39dd1T+DSoX18efughHZlVpkwZueyyy3TkXV9/840s+vFHHeFsSlzaSYpd3klH7kn6dbEcnGX+FIkiRYrIzBkz7MJxhcm+/fvtWXsvrZ5q1KiRJ4uwhYaGSteuXXXkO0jQUeACw8Ik5oVnJLBYUd1jTuq27ZLw0Eh7DzYA5IeadVQDRDhLLW18ctw4efHll3VP4aMGla9aP3+Y9fnoBjXz9cjDDzt6nrEJavZ8+EMPcexaLgUEBkrlsaMlKMr8Kox/sH5Pe8ZPklM74nWHOY0bN5Y3X3/dfs8UJqvXrJFB99zj6FLw/FArGkzUIMmvphdeKNWqunyqgQNI0OEJYVUqS/STj9sz6qYd/26eHPzwYx0BQN6oQkGxDu+7My0qKkouaddOR96jErBJkyfLuAkTCu1SZpUkPzNtmjRv3lz3uEMVy+vapYuOvEsVaHxnJjVlckstda/w0IP2vnQ3ZSUny87hIyTLhSJuqkL3+Cef9OwSa1M+/OgjmfL00zoqeF07d7YTdS/pf+ONPvm6IEGHZ5Tq2llK9rpORwZZHxZ7x0+S5HXrdQcA5M2wBx7wmZkb9e987eWX7UrwXqUGUl2sQV6tmjV1T+GiBrcPDx8ut916q+5xj3ruxz7+uGuz9vnxxLhxdq0A5E5U7LVSpO3FOnJP8rLlcuCtGToyR712VTHFxw2fduBFU6dP98wRhOomX6XoaB0VvKJFi8pVV12lI99Cgg7vsC6w0SNHSFgd85UWs5JPSvxd90mGH1c/BmBevXr17Dv0vkANXtVevJiYGN3jTWqQ9/vixfbZuoVpRiw4OFhGPPSQPDZqVIH93Or1rI518/rzvnv3bhk/caKOcFaBgVJp9EgJiIjQHe7Z/+yLkhJvfqm7urk14uGH5ZWXXpLIAvg5C4Javr1w/nx7ZZQXhISEyI39+umo4F3UtKlUrFhRR76FBB2eElS0iMS8+KwEFjdf8CMtPkF2jhxt75UCgLxQicyTTzwhVSpX1j3eo/6NI0eMkAcfeMCO65x3nv2nlxUrWtQu/vTBu+9KxQoVdK//UufcPzt9un3eeUEvEX1g6FDp0L69jrzrRSsR27hxo45wNuHVqkn5YUPsyRA3ZZ44IfFDh0umC3UD1LXu1ltukdmffSZly7p8xJyLSpQoIU+OHSs/LVok9evV073ecF1srGdqWVzft6/nbzZmhwQdnhNeo7pUfMJKnF0YpBybM1cSP/hIRwBw7tQxWK+/+qonl7qrWVk1I3r6rGy5cuXsP71O/Xu7d+8ufy5dKjf16+cTS6/zomrVqvLNnDly+4ABnhhMqlmw92bOlEbnn697vMk+dm3EiEJbqyAvyvTpLeEN6+vIPSf/WiUH3npHR+Z17NBBfv/1V/usdF9N0M5EJb6XXXqp/PnHH/LQ8OGe/MxRq3C8MGutKvur2gS+igQdnlSqy9VSsqcL+9GtD/Z9456Sk3GbdQcAnLuOHTvK1ClTCnz283RqmecLzz4rj44c+Y9/V6lSpXxq0Kpmwl5/7TX5+ccfpXWrVp56jvNDJcK39O8vS377Tdq2aaN7vUEdu/bF7NmeT9LVsWvz5s3TEc4mMCxUqjw1XgLcvtlljbUOPP+Sq2MttZXnu7lzZdwTT9h7kX2Zul43aNBAvvjsM5nz5Zf2TT2vUjcNenbvrqOCo66p6rPOV5Ggw5PU0SDqfPSwuuaXYmaq/eiD7peMpGTdAwDnbuDtt9uVhL2QQKol93O++kpuvfXW//n3qJkFVYHel6gB6gVNmsiCH36QL63EUSXqvkztjfzeSh5eefllz+wf/bfK1mtIJThqNtKL1GtCnUhQycPbS7wo4rzaUmag+0UIM5OTJWHYw5KZlqZ7zFOrboY9+KD8sXixdLvmGp+cTa9bp468/cYb9o28K664widuUHrhuDVfr2FCgg7PCipSRKpMmyyBLpwznLp5i+x6bIxd4R0A8kINBtT+3bdef93eQ10Q1L+h3/XX28vCs5uVVTMcBfXvyy+1xFMNUhctWCAL5s2TXj17+sxZ9Op3oxLz9999V379+WdpY/1+vD6AVDPpasZOFa8L99AWA1Uc64P33pPvv/1WGtR3f8m2T7Nec+XvulPCrETdbadWr5V9z72oI/fUrl1bZn38sfy4cKFccfnlnj/vX1HL8z98/31Z8eefcr11TfelLT5qmXv16tV15L7ixYv7xJGROSFBh6dF1K0jFceOsl6p5l+qRz//Sg7O/kJHAJA3ajC1dMkSV88bV3vN27VtKz8vWiRvvvFGjkv71NJqNYvuy1Ri29b6edVe6a1xcfLUxIly4QUX2M+D16iCTj2uu05+XLDATsx79ujhU8v01etl7JgxsmTxYunUsWOBPceqkJ76/p998on9PHa3nlN/2e7gNrXUvdK4MerCoXvck/jqG5K8vmCOuW3VsqV89cUXslzXtSjjsdUrau+2OmLxLyspV6uF1Gvci9e0s1Hv1dhu3XTkPnWd8PnPuCyD1TWy0tNlU5dYSd0Yp3uyFzVooEQPG6qjf1KVHze0ai8Zhw7pnjNrsHa5BEZG6gh/S3xzhux5YoKOnNUwbo0EGL54ZGVmSsKIR+Xox5/pHnMCrIFIza8+kYg6dXSPNx2Zv0ASBgzS0ZmV6BErMZPP/HvPysiQuM6xkrJxk+7JXsluXe2VDP4q7cAB2TVuovWcmF09YQ+IHntUgl04ocCEGe+8I/OsAYMpl3bqJDf3768j/5Bhvc++mjNHxowdK+s3bLBjpxWxPvNUovrIww9L8+bNcz0zpCpg/2YlXE4adOed0rp1ax25Tz2/O+Lj5QtrAP659bV23To5evSoftQ96uZByZIlpdlFF9mJeTdroOrLeyFPl2l9Hq9YscI+4mzBwoVy4sQJ/YgZatawRo0a9p7W/jfdZC+7N5GUJ4wcLenHjunInKjr+0jxVi10VPD2v/m2JK1YqSP3hJ9XWyrec5c9m1+Q1PVh7ty5MmPmTPlz+XI5fPiwfsQd6nqtiox2uOQSOzFv2bKlRPpJHqM+88aNH6+j/3UyOVm+tp57E5+L6satWl3ly0jQCwFfT9CVDOuDc/N1vSV1yzbdY05Y7VpS68tPJDA8XPd4Dwk64DvS0tLkj6VL5ZVXXpG5334rx62kJq+DkkBrQKtmNFUyrgYgna++2k5avL5U2m1qaHPw4EFZvXq1fPvdd/bge8kff0i6NS5RX05SM1xqoK0S8hbW7+Vq63eiiqupJN2fqbPIv7EG2LM++cR+bk+ePGkn8PmhXttqxUFrK1FRS1TbtWtnF8TyhSXJ8G1HjhyxrxOq8ODixYtl9Zo19rXCyQRSXSvUlhx1rbj8ssukffv29h7ziEJybvvpPps9W3r37asj56jnd1d8vM9sfcoOCXoh4A8JupK8br1s7XmDZCWbL+ZWsncPqTLhiQK/u5sdEnTAN506dUrWWAM/lbD//vvvEp+QIIlWIhlvDShUgnM6tf+3fLlydhExVfStWbNm0rBhQ2ncqJHfJ38mpFpjiZ07d8qatWvtPzfFxcnWrVvtgbmaSUtKSrJn4M9E/Q6iSpe2n3e1v7GalTTWq1tXqsTE2L+TypUqFcpB9t/Uc7fWel5Xrlol69evt2fP1Gykel63bNki/x5oVoqOtmcO1Zc65/6CCy6QmjVrSiPrtR1TpQoJOQqcWh2ybds2eyWOul6om1DHjh2zv9Q1Y/eePZJ8hvGoSgyjK1a0bzSp64W6gapu2Kmq8udb14oq1utb3YgqzNRnXYvWre1rhdP63XCDvPXGGzryXSTohYC/JOiKOrN8zyOjdWRWpWcmS+lruurIW0jQAf9xto9hZsfNy+1QiN/FucnpeeW5hK/KzfWC13f2Xnv9dRl0zz06co66sffDd9/ZBTh9HQl6IXBk9hdy4OXXdeSsWl9/biXoLt7ptl6uu6c9Kymbzv6ayq/AYsWk8phREuTB1xQJOgAAAHzJvn37pPEFF8jBs+R0eaG2C/y1fLlfrMAhQQd8EAk6AAAAfIVKOW+59VZ574MPdI9z1IqF1155xS4m6Q84nwIAAAAAYMz7VmJuIjlXypUrJ9fFxurI95GgAwAAAACM+PXXX43sO//bnQMH+vzZ56cjQQcAAAAAOG7V6tXSq0+fM1a9d0LFihXlvnvv1ZF/IEEHAAAAADhq/oIFcvkVV8j+Awd0j/OGP/igffylPyFBBwAAAAA4Ii0tTZ5/4QW5NjbWSMX2v9WtW1cG3n67jvwHCToAAAAAIF9Upfb169fL1V26yJAHHpCUlBT9iPNU5fYpkyZJaGio7vEfJOgAAAAAgDzbsGGD3DlokFzYrJks+vFH3WtOrx495IrLL9eRfyFBBwAAAACck0OHDsnszz+Xzl26SKMLLpA333pL0tPT9aPmRFesKM9Mn64j/0OCDgAAAADIUVJSkqxbt07eevttib3uOqleq5Zdof37H36wl7e7ITw8XD547z2JiorSPf6HBB0AAAAAIJmZmXLq1Cl7dnzDxo3y1Zw5Muqxx+TyK6+UWnXq2EvYB955p8z55htjR6dlJzAwUB579FFp3bq17vFPJOgAAAAAUMidOHFCWrdpI42aNJEatWvL+Y0by3U9esjESZNk4aJFkpiYKBkZGfq/dl+fXr1k6JAhOvJfJOgAAAAAUMgVKVJEdu7aJdu2b7eXs3tJ2zZt5OWXXpKgoCDd479I0AEAAACgkFNHl7Vu1UpH3tH0wgvl01mzJCIiQvf4NxJ0AAAAAIDUOe883fKGi1u3lrnffCOlSpXSPf6PBB0AAAAAIM2aNdOtgqVm86/p0kW+njNHSpUsqXsLBxJ0AAAAAIBUqVxZtwqOSs6H3H+/fPjBB1IkMlL3Fh4k6AAAAAAAiYmJkWLFiunIfSVKlJB3Z8yQpyZOlJCQEN1buJCgAwAAAACkePHiEh4WpiP3BAYEyGWXXirLly6VXr166d7CiQQdAAAAAGDPWru9Dz26YkV56cUX5asvvrBn8As7EnQAAAAAgK1Bgwa6ZVZkRITcPWiQrFyxQm695ZZCccZ5bpCgAwAAAABsFzZpoltmhIeHyx0DB8rKv/6S6VOnSslCVqX9bEjQAQAAAAC2OnXq6JazqsbEyNjHH5fNGzfK888+K9WqVtWP4HQk6AAAAAAAW3R0tBQtUkRH+VPJ+rv6XX+9fD93rmxcv15GPPywlC9fXj+KMyFBBwAAAADYihYtKuXymESr/2+d886TO++4QxbNny8b1q2Tt958Uzp06MAe81wiQQcAAAAA2MLCwiSmShUdnVlAQIBd5E3tH29/ySVy3733ytyvv5YNa9faRd+ee+YZufjii+395jg3JOgAAAAAgP9q2aKF/We5smWlcePG0rFDB7kuNtZeoj5zxgyZP2+erLeS8V3x8TLvu+/k6cmT5dJOnezl68yU5w8JOgAAAADgv0Y9+qiknToluxISZNmSJfLd3Lny0Qcf2EXe+vTuLW3btLH3qoeGhur/B5xCgg4AAAAA+C8S74JDgg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHuAjCXqA/b+zyTx1SrcA/5aZfFK3chDE/TcAAADAl/jECD4wNESCihXTUfaSVq3RLcC/nVy2XLeyFxJVRrcAAAAA+AKfmWILv6CxbmXv6NdzdQvwX1lpaXLsh/k6yl5o5WjdAgAAAOALfCZBjzy/oW5l78S8BZJ+6JCOAP907JdfJX3PPh1lL7JFM90CAAAA4At8JkEvenEr61+b80b0jKNHZfdTT4tkZuoewL9kJCfL3vGTRLKydM+ZBZUvJ+HVqukIAAAAgC/wmQQ9rFpVCa1eXUfZO/rp55L4yWc6AvxHVnq67Bo5WlI3b9U92SveqYMEBPrM2xsAAACAxWdG8IGhoVKqV3cd5SAjQ/aOfFwOvPOuZFltwB9kJCVJ/IMPy9Ev5uieHFiJeanePXQAAAAAwFf41BRbVN/eElSqpI6yp2Ya9z4+Trbffpec2rqNJe/wWeq1fOynX2TztT3lmErOz7K0XYlscZEUyUXNBgAAAADeEpBl0W3HqeRiU5dYSd0Yp3uyFzVooEQPG6qj7O1/5XXZN3GKjs4uIDhYIpo1laLt20lErRoSVLasfgTwqKxMSYvfJSc3bpTj3/8gKZs26wfOLiA0RGp8+oFENsw5QVerS+I6x0rKxk26J3slu3WVKtMm6wgAAACAKT6XoGempsnm2J6Ssm6D7gHwt1I3XS+Vxzymo+yRoAMAAADe43NVpAJDQ6TK1EkSWKSI7gGghNWvK9EjhusIAAAAgK8xm6AHBFj/y/lotP9KT9eNs4uoc55Umj5JAkJCdA9QuAVXKC/VXn9JAsPDdc9ZqHUzuVw8o7aJAAAAADDPeIIeGJm7me7cHB11upKdOkrF8WPsPbdAYRYUVVqqvf2ahFasqHvOListVTKOH9dRzoKrV9UtAAAAACYZTdDVOczBuai6rpzasUO3cslK/qN6XCdVXnlBAosX051A4RJap7bU+OR9e1XJuUg/dFjS9x/QUc5CKlTQLQAAAAAmGd+DHnZ+fd3KWdqWbZKyc6eOcq9E+3ZS68tPJKJ5U90D+D+17Lxk315S67OPJLxaNd2be8cX/y6SkaGjHAQESFiVyjoAAAAAYJLxBD3ivNzP7B3++DPdOjdhVatKzfffkehJ4ySkahXdC/ihoCCJaHahVP/4XakybowERUbqB3JPVXA//PEnOspZYES4hFU/9xsAAAAAAM6d0WPWlLTEg7KhRVuRzEzdk72QypXkvO/nWElBhO45d5kpKXLsx5/l4DvvyqnVayXzWO722QKepbaKRJWWyNYtpcyAmyWyXj0JsBL1vEpatVq2de9rH4N4NqE1qkudH76xZ9IBAAAAmGU8QVc2XdtDUlat0VHOyg65Vyrcd7eO8if9yBE5uXGTJC9fISmbNkv6sWOSlZqmHwW8KygyQoKKF5fwCxpLkSaNJaxaNbsvv1RSvqXvTXJy2XLdk7PSt/WXSo+O0BEAAAAAk1xJ0Pe9/Jrsf+ppHeUsMDJSqn/ynj1LCMBZB955T/Y+/mTujlgLCpSaX34qkfV5LwIAAABuML4HXSnZ+apcL8nNTE6W+Dvvy1PBOADZO7rwR9k3bmKuzz8Pq1VTIs6rrSMAAAAAprmSoKsq0EWvvkJHZ5cWnyBb+9woJzdv0T0A8sxKyI98+70k3HXvOW3xiLrlJrtaPAAAAAB3uJKgK+XuvP2cBvvpu/bI1tjecujzL3NVzArA/8o4cUJ2TZgkCfcMkayUVN17diHVqkqp2Gt1BAAAAMANriXokfXqSvHYrjrKnUyVXAx9SLbccLOcWLqMRB3IpcxTp+Tgp7Ml7oqucui1t3J35vnfAgKk3H2DJDA0VHcAAAAAcIMrReL+lnYgUeI6d5MM689zZiUNoTVrSNF2F0uRC5rY7aASJfSDQCGXlSnp+w/IqU1xkrRkqZz4dbFkWHFeFLHeY9Xfek0CAl27fwcAAADA4mqCrhz5YYG9F1bSz2FGLzuczQz8PwfeyoElikutObMlrHIl3QMAAADALa4n6CqJ2DPpaUl8+XXdAcALAsJCJebVF6V4uza6BwAAAICb3F/DGhAgFR4cIiWuowAV4BlBgVJh9EiScwAAAKAAFcgmU3UmeuVxY6TopR10D4ACExgo5R4YLGX69NIdAAAAAAqC+0vcT5OVmio7HxsrRz76RPcAcJNa1l5xzCiJ6t1T9wAAAAAoKAWaoNusb5/43geyb8IUyUxO1p0ATAuJqSyVp0yUos0u0j0AAAAAClLBJ+jaqe3bZeeDI+Tk8hVW0q47ATguIDRUSlzbRaIfe0SCihbVvQAAAAAKmmcSdCUrPV2OfP+D7Js8TdJ2xNuz6wCcERASLBFNGttL2iPr1rE6OKYQAAAA8BJPJeh/y0xJkeO/LpbEN96Wk38ssxN3AHlgJeGBRSKl2GWdpMytN0lk/fp2UTgAAAAA3uPJBP10qfv3y/FFP0nSb7/LyY2bJG3LNslKS9OPAvi3ACshD6tVUyIbny9F27aRoq1aSFCRIvpRAAAAAF7l+QT9H6x/qppNTzt8RDJOHJf0g4ckKzNTPwgUXoHhYRJUvIQElywpwSWKSwCz5AAAAIDP8a0EHQAAAAAAP8U0GwAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAFTuT/AEi4PhsWDpChAAAAAElFTkSuQmCC" + }, + "9f0d8150-baa5-4c00-9299-ad62c8bb4e87": { + "name": "GoTrust Idem Card FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "12ded745-4bed-47d4-abaa-e713f51d6393": { + "name": "Feitian AllinOne FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "88bbd2f0-342a-42e7-9729-dd158be5407a": { + "name": "Precision InnaIT Key FIDO 2 Level 2 certified", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "34f5766d-1536-4a24-9033-0e294e510fb0": { + "name": "YubiKey 5 Series CTAP2.1 Preview Expired ", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "83c47309-aabb-4108-8470-8be838b573cb": { + "name": "YubiKey Bio Series (Enterprise Profile)", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "be727034-574a-f799-5c76-0929e0430973": { + "name": "Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXQAAAF0CAYAAAAzY8JTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAf6klEQVR4nO3dP3Ybx5bH8bbeBJO5ZwWGVyB4BYKyyUStwNQKTK1AVDgR5RVQXgGpbDJSKyC1AkDZZICymYhzWr4tN0GQQHfdqrp16/s5p47k52cS3QB+KNz691ODQ8ybpmkH/7+ZNADx3DZNsxn89JU0PIJA/1srod21XwYBPrfw4AA8cC1h/2UQ9Ne136ZaA30h7bmENr1twIdbaZ8l4Kvq0dcS6HMJ8FfyJ4A69D33LuAvt0o47ngO9C7Ef2+a5ogeOADRhfsnCXd3vXdvgd4F97EEOSEO4CmXEu4fvdwlL4F+NOiNA8AYGwn396X32ksO9FZ643/QGweg5FqCvcgZMyUGehfkJxLk7QH/fwAYq5sp82dp5ZiSAp0gB5BaV4J5U0qPvZRA70orZwQ5gEyKKMVYD/Ruzvg5NXIARnQlmLdW57M/M/AYdul64hdN01wR5gAM6aoFSyn/mvMvg4/pSIKcfVQAWPTvTdP8p1QQur1k/sfKY7RUcmmlvMJccgAl6UowHyw8XiuBfiRhzqAngBJ1g6Wvc9fWLdTQz6ReTpgDKNVCautZKww5e+htIbXyfppSt9Dg247/HYC+7fMIfpEJEiWcU9BNbzzN8YtzBfpcwtxSr7zfR/nL4O+ut9oECjYbHErzXHrIlvLkUhYkuc+QbtrPummau8xtORiEpdwDlG8u0wkvDORL1268T7s+MRDiZ0yJBKrQT7bI2YFce82b84w39JwQB6p2LGVeQl1BjjBfypNIOQVAb5ax137s4VlIHeZLLzcOQDStzERJHexFZ1PKMCfIAYyVI9iLzKlUYb4myAEEahN3QIva4iTVjWGPdACaFjLdMEVHtIiB0uMEN2MpNx4AYjhJUIYxH+opwpxeOYAUZgl660ureTaP/Im2plcOIIOzyKF+Y+1JbeWTJuYF0ysHkMtR5A7ruaVnNuYKLFMXCqBasUswJmbrnXq/QAAQbcQObPZB0gVhDqBCsaZmZysvx6qbu92dDIArsUL9LMdNirHnMGEOoCSxQj3pjL4jwhwAvosR6snmp8cqtRDmAEoVI9STlF5iTLJnABRA6WKEetTSyzzCAz7hZQzAgTbCPPWoq0i151+yaAiAJ22EFaVRKhjaA6Es5wfgkXYlYx0jKzUHQteyjBYAPDpRDvVTzXukvS1uUad1AMAEmmt1VHvpmr1z6uYAaqBdT1fppWv2zs1u5g4AEWiOPar00jV75xxQAaA2mqWXoF66Zu/8gpcxgAppll6Ceula886Z1QKgZpqzXibNS9ecS6k65QYACqRVvl5OuXStfQmiTIoHgMJoHgg0ajxSs+bDxlsA8DetMvao6d9ag6GTvhoAgFOavfSDKx9a02zYSREA7tPqpR9U/WiVfhm1cwB4SKsCctBUcK1fxswWANhNa8bLvU7zsx2/6pXSE/CRJxIAdvpL6bY8udGhVrmFVaEA8LgoWbvdQ9faa+UTTyQAPGrTNM2lwu15MrM1DoBe8xwCwF5a45U/Qj1GD13jUwcAvNMaZ9yZ21o1HU4jAoDDaKz5udr1m7Q2YmfuOQAcRqvs8t2w5DJXeAKupdgPADgsMzV8z+9hoL9Q+KGfeQIB4GCrpmluFW7Xg0DX6qEDANLm5vNmEOgzpdo3gQ4A43xRuF/3eugax8MR5gAwnkZ23gt0jXKLRh0IAGqzUphM8r3C8mz4D4G+8jIEgEk0OsSLPtA1ZrjQQweAaVTyc9f2uVkfEABU6JvCJS80a+gsKAKAaVR76KE19JXCYwGAWml0iH/RKrkQ6AAwnUaGzjRr6ACAaVQ6xc+U9kCnhw4AmWn10JmDDgCZUXIBACcIdABwgkAHACcIdABwgkAHACcIdABwgkAHACcIdABwgkAHACcIdABwgkAHACcIdABwgkAHACcIdABwgkAHACcIdABwgkAHACcIdABw4t94IjEw5XzZ26ZpNtxEID8CvS5dYM+kPW+aph38s4Zr+RldyH+Tf15xiDiQBoHu11wC/Ln8fZ7gShdbf74b/LtrCfov8udtPU8FkAaB7kcf4K8mlk5iW2w9ro2E/OdB2AMIQKCX7UgC/EjKJyVp5XEfyWNeSbB/aprmsvYnFpiq6zXdBbZT7n4yXQCeN02zVnjeLLeLpmmOK3lOgUbhvXjVEOhF6MopZxWE+K62lg8wi2UkQFNwoDMP3bauh3oj7aTAsoqGVu5D1/tYVnwfgL0IdHta+cbT90xTzE4pxUy+qSzl3mhNtwRcINDtmA1q4+/ohT6p77UT7MAAgZ5fH+RLBgEnIdgBQaDn0w7KBwR5OIId1SPQ8zgdDPBBVx/sp5StUBsCPa0jCRtq5PG949sPakOgpzGThTIXlAOSaqUEc8VsIdSAQI/vROaRH+V+IBVbyHPAAji4RqDHM5Oe4RnlFTPeSbDTW4dLBHoc/QpPlqvbMx+svAVcYbdFXX3NlvKKfWeyU+VrTlx60lOdkg3bHttCoOuZFzjo2Z8m1J8wNDxO7qmj5YanHLVy7T/Ln5onIMW2kJkwLysOpuEpVi8G/9sUnFhlALsthjsuYNfCpXzgnCQqBS3kvpxJicP6/alheuNMrvM88XOylvGk00L37k8l9D6zfa6Cc8Mh1Qe4hR5zf6BFvzrW4v06N3CftFm95/2sIwao/xF6Twn0AK3cQEtvknVBNfzZYEqnpXt45aAHWdohKEv50Kk93DVeuwT6BHNjQXQlX6VLDaKZsV7kTYELwGaDLSWsvC6ntH7bhhoX4GnkAIE+0txIz2ctIejthX9k5JvPupAe40JKayWF9qHtorKpv6H3i0AfyUKY13Jqz8zA+ITlUD8uZLBZo91UMmgdeq8I9BGOMod5rRtN5Q52a6F+7KCswntgt9D7Q6AfKOe0xDXTQr/LGewWQn1RUY98X/O6Cjv0vhDoB8gZ5uwD89AiU409V6jPDM6mstK87V5KoEeWq2bOdq/7HWd4blKH+imhXVX+aOQGgf6IHGG+ZtOoUdoMMzxShPqi4jr51OZhF83Qe0CgPyJHmF9x+MVkqQesbyKWwiyvPC6hlZxFBHoEbYbBJ3rl4VKv3L1RfvwzBj1Vn5sSO0eh102g75A6FKiV60pZd9ba+yXHeID3ti5wG2sCXdlZwhf5BTNYolkkDMjQb1eUWOK2krIp9F4Q6AMppycyrzy+lPvtTJkTTYklXTsvpPMUek8IdJFyELTG1Z65pBgPWU8I9NyrjmtsMQeytRDoClINgpay2ZNHscoaUwbfUpb1aA+fL8uhTqArSPEGI8zz0w71s5FXZHH//Brb0vB7kUAPpHHthHk5NEJ9yuyJlIO0tHLfkwR6gDbBm4wwtyck1KdMM2X5vs22NjhXPfReXdV86n/sja82Dk6T3zfYd73n31v0Rh7T2MHpj03TvJXn9RBthQc0lKR/fl6OeE7NqzXQF5Fnm5QU5jPpdXbthbzQx/ZCb+WaPzdNs5KgX0V6vBreyHUfErYbCfKPI37vouB1Bv1z93Xwgb3a83z293Eu1/xi8HfL5lKmcBXqNZZcYm98ZLnM0sqH2Xnk+7CU32H1rNNDZjfVUGK5kMVRMV6zs8FrzfIYwkWEa58i9DqqrKHHfsNZnGfeh3jOsycvDIb7U+MoYxej5Nj5cWo7z7Qsfi6lTovhrrWNQ4jQa6gu0GeRX0zW7sPMYO9oLY/JyoDUfMfjG/uhPC9gu1trZ9FaOQx82HJ3xkIff3WBHnPfDCtf2xojBywf0qwE+8kg9MaWHk4M39/+TW55YNbSazX3rDSN57qaQI8553xppOdTSpBvNwvBPrb3ar3EUtq5mxZeu7l3Pw19/FUFesyvd7kHQVsn851PC5kZYrnEMqVkZEmuw7AtbOAVeg3VBHrM3nnuwym8HVe2NN6ztFxi8XSo+EmisR9LH4Ch11JNoMfqnV9lvi7PGz2N3SslttZwOWvKjo8liL3FsLUDZkKvp4pAj9U7z7l0uJa9tK0cJZZyb/Wx7aqCg1JilBMtLvzSeC24D/RYA1e5Si21bfSUe+aB5ePhrH2LiUlzD3mrZ/iGXpf7QJ9FeiNpHxB8qJSnKllrqeuclkssOe6HBaEH0VjeOrdReE24D/RYb8gc9cqaw7xvqULMconFa738UFMPpClhb53Q14brQI+1PW6OBUSccvNPix3qlkssbMf8t7GhbrXEsi309eE60GP1aFMP0tEzf9hihbrlEgthft8hoV7aPQt9jbgO9BhfmVNv4EOYP940Q72EWUPsq/5Q+8QajBJn/4S+RtwG+vaGS1otZe/8yGEIWwy5Ek7gr3EA9FC7BkpLPRIz9HXiNtBj1JxT9s5DR/NraaFrAUoYm6hpauJUfYaVPmBMoD8ixlL4VL3zqaP4tbYpU0hLWZiVeyVySbpvWqUvsAp+vTwzcBHa5hHC9zLhkWpnDH4drDs27P3I/+bI4JLvXbpre23vYZnVvUfdHCM3lcdA/z3Cz/wzws/c5Yh66cG6c0x/kzfyoU4LOuvzDQGFKbyVXLTLLctEjzvWvHmPbWxduTV4Os5TzcJxaEgvuOTi7dT/GOWWv5R/3mPeVbDJUqiN9FzH9MpLO4G/u8a3Bh4HCuSt5BJjhPtjhJ+5bV7QarZcuhLLywklltLmI7+l1IKpvPXQXyj/vNtEg6FMTXvax5FB1x8PV9oUtttEHQg45S3Qj5R/Xopyy4JVgI/qyw9jQq60EssQpRYE8VRyiRGKY77eT/Uuwe8oUV9iGRPmJwUf+HAtDZjMUw9dO9BTlFvone82pcRyHuEbWkqppsbCMU+B/lz556XoLf2R4HeUZEqJZS4lFgtH1U21SvRtEM55Krlor/z7pPzzts0K71FqW00ssVg5dzQEvXOo8NJDbyO8qWP30Anzf1yOXBnpocQyxMwWqPAS6Nq9c8ot6XQllg8jfpuHEssQe5BAjZdAjzEgGlOMFa2lWcnmU2Pu9bHDZfGxS3uoiJca+i/KP++z8s/bVnu55VI21jo0zPsSi8c9ThgMhRovPXTt3m7s6YqvIv98y6aUWM6dbil8TbkFmqih7xaz5NJWut/5RmaxjC2xnDnetIxyC1R5KblovuFjD4jWGObdPf11YonF8w6UscdqUBkPga49IBr7K3BtK0PfS8/80Ps6l+X7NRz0wVJ/qPK2OZeGL5F/vvaKVqv6I9TGhJb3EssQYQ51HgJdu4QRu4deQ8nlWsJ8zL08q2xP+FRn1KIiHkou2r252HVN7/PPx5ZY+hP4azvg46uBxwBnKLmk5bl3PuV4uKMKBj4fw4Ao1HkIdO1FRbGnLHp0KyWWMWWE2kos25h/DnUeSi7aJQzeaON8kFWfh4Z5rSWWbdTQoY6SS1qepixOKbE0cg8+saiGQIc+Ah1TTCmx9NgqFojE0wEXGii37De2xAIgEXro9zHz4HFTjocDkBA9dBxqxepGwDYCHYeay+yU2vdyB8wi0NMqvaTTyvFvZwYeC4AtBPp9sVdyehl09XLaPuAKgX5fjUvQp5pagun+/+umae4qb7Vto4wECPS0vA0qTinBjD1P1Cs6D1BHoKfnca57X4I5NKRWEupjzhb1psaTqxCZh0DX7unFfqN57Zl29205spTwdsK+6V7UctAJEvIQ6N+Uf17sr8KeSw2tHB93OuK/qbUEQw8d6ii5PBR75kbsI+4seCfBPqYE87KyVagz6ujQ5iHQtQcaYwd6LastFyNLMP3ujW8qKsEw0wWq6KE/pH1gxrZVRRtbTSnBfJTeeg0lmBcGHgMcoYf+UIrFMrXtifJOpjceWmK4raQEwzYKUOWlh675FT3F1+AaD3c4kqmNhw4G1lCCmbHaFpq8BHppUxcvK52qN+X4Oe8lGHrpUEOg75ZiStnYo9s8OZtYgvF4z/4w8BjghJdA/6r881IMVv2V4HdYNqUE81oWI3kyY046tNBD3y3FG+yaY9wmlWA8HoFHLx0qvAS69qyReaLBqj8T/I4SdCWY85ElmN8clWCOWWQEDZ7moWv30lPMdvnIwdQ/HMuc9VpLMGO+pQA7eQp07V56ijr6hl76PXMJ9eMR/42XEswf9NIRylOgf1b+eammk32gl35PK+WXKSWYkhdstfTSEYoe+uPaRKHehfn7BL+nNFNKMC8Lv5fvWGiEEJ4CfROhjv5K+ec95gMn+Ow0pQRzKsFe6rceDuDGZN4259JeUp9yFZ+3+dVahiWYQ3Xf1n4ttARzxOpRTOUt0LWnsbUje4chris/km2fY5mzfmhJouQSzJjxA+CehcLp62O2R41tqXya/FXCx95GePze2npCD3Yh/11J9+Ii0msMdgVnlcf90LV76YuEA1X93Go8rpWwG1Nr7kswJY1THCX8dggnPAZ6jD1SUi7NvqWefpATKcEcaiNTG0sqa52xzwvG8BjotxEWmaRemv2hsvM1p5rS434r34JKmAXTjtyVslbnxsq+WXmroTfyeLRrmjmu8Yaa+aNtTO98l1lB9/eGUH/U2eA+jTmY3KLQ18n38T6PgT6L8KZaZ3ixtIR69IA7M3A9h7SUg/OlOH7kfVrq4dsqrxGPgd7IxWm/qXJcJ6F+v8XorR4VMgtmzFx873aFee73aqjQ14frQN/3hE9pOXrpDaH+o8UsPcwLucfMUT/8vV3a+EPoa8N1oDeR5nTnutbaQz1FHblflcq9sGtsiWxZ0Eyh0NeF+0CPMTi6zriBUimBo91S90qPCyjBjDm+z4PQ134JO1mGvibcB3ob6Y2ZexVfjA8qqy3Xa6uEEsyUVbMl0pqRZL0EE3p97gO9iRh+uUfSS1zOPjasct/jUr4Red6hUXvA2nIJJvTaqgj0WL30pYFP+37hifXAGdus9aQowaQX+7VtsQQTek1VBHoTsZdupWd05GRTr6XhEsK8kHt86mDANNUHqLUZQ6HXU02gx+ql3xlaxNAWXlsvIYhK+Ua0LHRjr0Wk9SNPNUvfbEKvpZpAbyKGnYXSy9CssJkw5wUeu3ZSyL0tJdgXmT8o10buU+h1VBXoMXvpFveunklJyGLtdy2PzVKQj30spZRg7gbBbu0b0FGGHvlTLXcJJvTxVxXoTaTVo32zOs+1HZz2k/sNc2M0WObNtDd0aYPSa7nGnGXCmeSF1Q/DnGM4oY+9ukBvIgeb9VkGs8E+4qneIDfyO62WVbZPiZpSUy2lBDNsS/mWlCLA5pIR1uf1594rJ/TxVxnoGtf7WMu118sUrbyZz5S/9l4NgqKEe7Hr2qfUVOeFrwvon7fjwB78TP77U/mZpdyT0O2YNQQ/hz8NRpZDvC8s1M8ilkhu5XDiEg5Q2DaTNh+E8fMdwdxd25fB3/tDRbQPFontfE9wf5QDMQ59LvsSTKnbt27bDA4R6Z7br1v//ufBt5lZgYPbvY0cUZj7PXsX+N93Ry1W10NvIg+Q3rF3dREOLZNMKcHUtDVD6W3NtMXyA72RkkDMFyN7V9s1dnB8SgnG+9YMXpqlb1MEeqDY87UJdXtCZjqNXRncGpuWR7vfrM3RJ9ADbc9wINR905i2ejOhVkwJxl6zuOAq9D5VH+hN5FkvfSPU89NcgzBl21pKMHaa1dWzofeIQBcpelCEej6xFpSNLcFo7etNm94sb4VAoCtKUeu84jzI5GKuDr6bWIIZe4waLbxZ2F9/HwJdUYp6+l3l50GmlmqTsimrLWN/0ND+aZamJj6FQFeWarVfKS+wUqWcXRJSSqOuHr+V1IEKvR8E+g4pe04lHFxbmpS7IGosIKOuHq9ZO8BiH5XXI4H+UMppZtYPri1JyqPitHt+1NX1mpX9zccKvQcE+hNSHhKxdLT/Rw6pt7GNVTLTPhC5xlby2aoEemSpT/45o7c+WuoQjD3+4fXg71Tvn5KFXj+Bvkebob5Jb/0wuYIv1Vd5euuHtxsn75nQ+0CgHyBHqN9JWJW6HWlsJ5nCLnVdtqW2/mRbO8ue0PtBoB8oV6ivCzkNP5VFxqPLcg6yzdnk60Er8XDxfULvCYE+Qq5QvyPYfxzCkis8rMyYyH0fLLQrx2s4CPTEcoZ6jcFuIcAsTn+rMdivKhhb0rhHBPpIbYbZL9ttLbVVrzX2YyOLbazPZV5UMCPGY2nlMaH3ikAPkDvU+3ZR6CKKbTP5kLIws6OEjZyGLN07jbaUge/aSoyh945AD2Tp4IK1fMhM2Sgql5m8cS0tfS99n50jeR2UFu7967fmPY5C7yGBriDlcvOxb45jg72cubxeLO5f4m0nTOvhvpRvFqy7+Fvo/STQlaTcEGpqUJ1JwKesR7by+jqV0pDlXmNpGzmNNZdvQzmfh+Wgo8Eai4dC7+/Vv1m7okLdNk3zm7xZLPY25ju+yl7L4/4mf2/knzcTf347+POFvGFLedO+bZrmg4HHEdOttP46Z4PXxfPBh6+GjfyuVdM0XwevtSmvLYzw02AKVIj39NJ/6O7DOyOPJcS+N6CHr8nd9b0efKDhb8Pndr7nm8tK2vbfMd5d4D37/jqm5KIv54pG2oFfT1mBC2OCSy7PeEajuJYSzKXDayvdRkosLykBwBsCPZ7+6/xrgsOMWwly7/VyVIpAj6/rpf/aNM1H7xdqWN8r/01CHXCJQE+jC5Q30jskUNK6lCCnVw73CPS0+tr6W8ow0a3kA/Q1My9QCwI9jw9ShqHXqK8vr/zKdETUhkDPZxg81NfDbWQ9BB+UqBaBnt9K6usE+zTDID+llIWaEeh2DIP9A8G0F0EObCHQ7VkNSjFvGdB74FY++P6DIAfuI9Dt2gwGT19WXo7ZyPX/Jo3SFLADgV6G60Gv9E1Fc9kvB2Womq4bmITtc8vS91Q/yvanR7JVbUmnFO3Thfgn+ZNyCjACgV6ulZRkPgz2sn4lf5Z0eMCtfAP5zGZmQBgC3YeNhGEfiDMJ9ueyn7Wlvcv7ww4+y9/phQNKCHSfVjsGDvvTaWYRTqjZpQ/rL4PTa6iBAxER6PW4fSJQ+2BvJ5663i+x3xDaQD4EOpqtPU+oYwOFYtoiADhBoAOAEwQ6ADhBoAOAEwQ6ADhBoAOAEwQ6ADhBoAOAEwQ6ADhBoAOAEwQ6ADhBoAOAEwQ6ADhBoAOAEwQ6ADhBoAOAEwQ6ADhBoAOAE1qB/jMvCADI65nSob5TDhYGACh6Jie1AwDyaTV+s1bJReXBAEClNKocG61Ap+QCAHl96QP9micCALJZaPxizWmLKg8IADDJbR/oGjNdZjwHADDJC4Xb9qOG/k3hhxHoADCNRn7+6Jh35ZK7wHbFEwkAo7UK+du1HzV0jbnozHQBgPE0xh+/T2zRrKG3hDoAjKaRm6tma5aLxtRFZroAwDgaA6Jfmq1A1+ilazwwAKhFq9QRfpDfxwpF+TUvQwA42JHWgGgToeSi9WkDADV4pXCNP3rnw0BfKc120XiAAFADtRkuu5wrdP2XvAwBYC+N9T93UrbZSaOOfkfZBQD20uhA3z21fflM6Rec81wCwJPWClm7d4X+jcIvWXPoBQA8SqsacrLvFp8o/aJjnksA2OlKKWf3buqlVXZhcBQAHtIaDL059N5qlF3unhp9BYBKaQ2G7i239LTKLmypCwD/0KqAHFRu6Wn+UqYwAsDftHrnozvLF6nrPADgmGZHefSkE61NYyb9cgBwRmtmy+RNEJdKD2DJvHQAFdOa2dK106m3UWtwNOhBAEDhtDrHdyEHSrdKy1ODHwgAFOpUMUODt1XRfDBMYwRQk7lifqp0irV76QdPhgeAwmkt0lTpnfc0e+l3SqdcA4Bl2rmpVrLW7qXfMOsFgGOas1pUe+c97U8b9kwH4JF2B1i1dz6kOfXmjgVHABzSrJvfxZzyrbl6tG/s9QLAC629WvoW/bAgreWrwwfMICmA0mkuxExWxZhFqA9xZB2AkmkdKTdsydbtaA+Q3jHzBUChYoT5OvXKeu3CP6EOoDQxwvwuxwLMeYTSC6EOoBSxwjzbFikxBgH6UGegFIBVscI8eallm9bJRrsujFAHYE2MMcS+ZT9Yv42w4GgY6iw+AmCF9jzzYTuzcpGx6ul9M3OhAKo0izQRpG/mzl6OVVPq2xWDpQAyWETusJpdh3MWOdTXFmpMAKqRItNMjxXGrDH17ZzeOoCI5pFLLH0rYowwxY2gtw5AW5ugV15UmDdyU1KE+p3U1jl8GkCo44gz9rZbtC1xY0kZ6ndShiHYAYy1iLCL7L6sKlLqUL+Tr0sEO4B9Ugd50WHeyxHq9NgBPOY4Q5C7CPNerlC/kyeO1aZA3WZSt05VI99uxdXM92kj7vtySFvLJyQzY4A6tNKZy5k7d947lCnmqe9rfbgfM58dcGUuu8DmKKlst2qmVsfeJmBsu5HB1GN2eASK0crA5on0wmMuzx/bljmy5KeMz9xCngSrPeTbpmk2TdN8ln++Hvy7jfx7AHG0W4E4k/az/O8zw5Meuqx4LTmRVM5Ab+QJuaBXDMCJD03TvM11Kc8y38NV0zS/yU0AgFJtpFeeLcwbAz30oQVzxwEUKFuJZVvuHvrQtfTWP9p5SADwqI30yF9aCPPGWA99iN46AMsuJcxXlh7jvww8hl26m/RX0zT/J+EOABaspLzyX1Z65UNWA73zv1KG+WvHFCYASGkjIf7aWq98yHKg97ob+Unmg1ueewrApw8S5P9t/eqs1tCf0pVg3lGKARBZN0HjveUe+bYSA73XBfofbLYFQFFXEfhTeuXmauT7lBzovZkEOxttAZhqJb3xyxKDvOch0Ie6UP+dcgyAA2wkwP/a2qupWN4CvTeTUszvzI4BsOVSJlq4W8ToNdCH+nB/Qb0dqNJKeuCfJMzdqiHQty2kvZDeO3V3wJc+wD/Ln8XMUglVY6Bvm0mwzyXkWcQElKMP7K/y99uSBzVDEeiPGwb7dk/+OT17ILounL8Nfslq0Nt2MYipqmma/wfd9StxQsbrQwAAAABJRU5ErkJggg==", + "icon_dark": "data:image/png;base64,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" + }, + "58b44d0b-0a7c-f33a-fd48-f7153c871352": { + "name": "Ledger Nano S Plus FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "07a9f89c-6407-4594-9d56-621d5f1e358b": { + "name": "NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d61d3b87-3e7c-4aea-9c50-441c371903ad": { + "name": "KeyVault Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "b92c3f9a-c014-4056-887f-140a2501163b": { + "name": "Security Key by Yubico", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "54d9fee8-e621-4291-8b18-7157b99c5bec": { + "name": "HID Crescendo Enabled", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "20f0be98-9af9-986a-4b42-8eca4acb28e4": { + "name": "Excelsecu eSecu FIDO2 Fingerprint Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "ab32f0c6-2239-afbb-c470-d2ef4e254db6": { + "name": "TEST (DUMMY RECORD)", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAA+dJREFUeNrEl09oXFUUxn/3vvfmjzOdmZcmcSakmUyGqoQolBQXMV2J/7DulLYGFHFRN0J0IQhSUAp22Y0utBZLsaJYMGhATV1INxJr1ZKmNqUYM5kYk2kmMzGZmffvuhhJtULmjQ7NWb533zkf3znfd94V05l+gMeBV4F7uT1xCTgGjIvpTP9DwFdsTzwsgeNsXxyXQHYbAWR1wAaCvj8RApTCW9/ALZfBdRGBAFoijggGQalmANg64Pmureu4xSJ2YZlAupfonvsQwSBucZXq5Su4+XmM7l2IUAhc109KT2+muL34OzIcouvYUcxnRzCSyc331anLFN5+l5V3TiITcXTTRPkAIaYz/SUg1uigWywS6E2T/Xocra0NgI3vvseanSPY10t4cA8AxQ8+IvfcYbQ2ExmJNGpJ2T8Dmo5yXaz5BfSNCrnDL7L25TmUW0VqISLDQ/ScPoE5cgCnUCA/+jLBvt2tY0DoOs7KCgiJnohT+2UWoyuFCBgoy6Gau0pkYC+7J88jwyFm9u6jNnMNvX3nlgxIvwwox0FLJJABA7dUJtCbRug6eAqha4SzA6xPXaD4/mkAYvsfw11bbZhXNqVaz0MEg8hoBLxbxKMUGiHWv50EINiXBtwWA5ASZVko2wYp/+UPChstGq1jrVq+UurNGJCyLFTNQjkO0vMQ4XCdCSlRGxsoPBIHnwSg8sOPCAItBADYuTl6Tr0HmkZ+9BWklAjDQFkWXqVK6sgbRPY9gLN8g9LZMfTOzha1QErsXI7I0BDmM09jjhwgcv8gTuFGne5SmUAmTfL11wDIPf8CzvIyWmxHixhwXJRtkzx6BIC1Lyb445vzmxLTEgmsuXlWTp7Cmp2j/NnnBPqyLXJCIbDzeSLDQ2TPjQOKmcFhqlPTGLu66zMgBHgKZ2kJ5XkYqeTm0moQPpxQKbzaOuahAwCUPhlj/eIkoczdN6WoFEjQOtoRQtx81goVeJUKgVQPsf2PArB69lMEBgjg7zUUCNmcqn0NoVsqE+y/B/3OTpRlU/npEnrbzmb3/n8HoCpVgtlMfeVe+RlncQkZDrXsl6gxAFyM7q66D8wv4K6t1XdAi8JHJg8tYdbbUShQc8rwq3vLAPwztDYTvb0DZVutASDvCAMQfeRB7jrzMXJHdGttjY2z8uEZjM5UKwAoMOrHjGSSxKGnGvvWcoGlE29hkPr/RqRqNYx0D3pHu+++Or8tYucX6n/JPoxoy0GUkSi1q9eoXLjoG4AWj6OZJsqxG4pAb9QG5dho8RhaPNbUdPsoDmBI4Po23oyuS+ClbQQwqgMTwBN/Xc8HblPhKeBNYOLPAQDIsXqbsqZKGwAAAABJRU5ErkJggg==" + }, + "30b5035e-d297-4fc1-b00b-addc96ba6a97": { + "name": "OneSpan FIDO Touch", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "6d44ba9b-f6ec-2e49-b930-0c8fe920cb73": { + "name": "Security Key by Yubico with NFC", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAfCAYAAACGVs+MAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAHYYAAB2GAV2iE4EAAAbNSURBVFhHpVd7TNV1FD/3d59weQSIgS9AQAXcFLAQZi9fpeVz1tY/WTZr5Wxpc7W5knLa5jI3Z85srS2nM2sjtWwZS7IUH4H4xCnEQx4DAZF74V7us885v9/lInBvVJ/B4Pv9nu/5nu/5nvM556fzA/Qv0Hb/IrX3VFKPo45cnm4inUIWYwLFRmZQUuwjFG/N1iRHh1EZ0NRVRudqt1Bd+2nSKyS/Ohys0+lk3e/3kQ9qvD4ZUta4VVSUuY0eipyiThAfocoORVgDuuw3qKRiAd3rbcEtjTjYIof6WaHsCmzVPWCMx+cgh8tLqWMKaMWsUjLqo2RtJIQ0oOzmerpQu4esZgsONkGxH7d0kdvTT17s4OMU7VI8ZhjgGaM+Aq9iENu8Pif1udz07MwvKWf8GlVoCEY04PC5WdTaXYFbR8vNvL5+3Kgfb5xNMya9RamJiynaMlGTVtFlr6ba9u+pqnEX4uMuRRgjSYEhrN7utFFe6lqal7Nfkw5imAGHynPpbk8VmY0xstnptlFCVCYtzTuBN83QpMLjTtevdPzSUnJ7e8mkjxZ39fXbKDfldZqbvU+TUgGnBVF6fQ2iPHg4W16UWUwvzbk16sMZE+Pn0pvz7JSeuAyes8lcpCmaKuo/p+qWr2UcwIAHWrvP0YEzhXAtLAbssHhp7iGamvyijP8ryqrXUWX9XoowxyAufNBrp43POBFXZlkf8MDRiqcpyowAwpuz2x+fWvz/Dtde9smszygtcR6C1wbdzBl6Olq5WNYY4oGathJMrkTEx0jARSHAVs+5rYkQNXb+QgfPLsQ6gXyInsreQfmpm7RVFYfL86n1fiUOkYvShkUPxvbukzoy6K1ihM1ho3XzW6EvSfXA+dpiWGaWd+doXzLzmGwKYFLCAsRAlPBAhMlCFXU7tBUVPr8HgVcJHWq+F00plr+DMTdrP4zvxY11kNMhxT+SeTGg+d4V5LQJityUGJNB8VFZsjgYBZM/II/XCTkj0qyDOpF2AVQ17CIjUp/DnT1UkL5F5gdj+sS1wg1gE3gigm60fCXzSnPXbyAPbIXv+IDpE16ThaHIS9skyhlmME5F3cfqAKhq2C0E5PH1gYaXaLPDkZG0HDJOnKWHp51I0z5SOux8e1WAuZzdHQrTkp8TmjXoI+la0wGZszubqbO3ifQ6A/W7vVSYsV3mR0JKwkKc4WHiBkmR8I3CCgI87oOL4qzT5P+RUJBejEOgAPK8hYPzatM+eITp2IO9yTQmeromPRxx1qxAcsile/ubSeEbcWQGYECghcLY2HyKjogjH25hMpjpUv1Ougli4eh2eRw0O32bJjkyuCgNzg0vzlYMSiSs0uoo4MG7hMOjCEaX1yFE0nSvjBzuTnEpK86Z8IoqFAIubw8kg9ArEaREWSZI+jH4Xbp6g9E9EnJT3oaRzDN+MUJBQDHn56a8oUmEBusOxBs/N5+tJEbPkAFDj8UGvOs/IWvcSglGBhvS7/FTYfpWGYdDY8fPAxWSA35sTC4p4+Lm4AaqIoPeQtfufK6Jh0ZhxlbsUXOSmXNifD5ZTAkyDofbbcclxnA8WNAqxCbRNykhXxQpaDw67fXUYbsiG0Khtv2oeIvh8rhQMYOcEAqXG/eI+zngOc5yxr8q82IAM1c/FLFOplqu5eFQXrMZzGcVCjYbLWG5I4BT1euRrlbxtNOtMitDDEhLXIIynAAvuOEWE3X3NdAft94VgaG42XIQt0ZX6PeCE/qQFe9rK6Hx7YU50KvH7fW4fS+q7KKBJxsggBX5pSAGh1jIrVh5zQ6w3RfaahBXm/aCbCZTjCUFUTyWZqW9p62MjJPXVqOrPgMO4Nv74Gkf+owftNVBDQnjFJqHSw17pXvhWW5KZqe/Q49N/USTCAVWoQXFIHBHXXe3FPrUDsuGDmtF/hHKTHpekxhiAOPI+SJq6S6HF4I9YWzkBJTo46iUMzWp8Pir/RiduLxKYsSksV8vLlOQvhGX2YlR0OBhBjC+u/gEcvY0ApK7Yk41NxjPSQnWFHTF66UrjgevB8Cu5a+l2vYSRPtuVDo73hhdMSHnUX7tTjsVZGxAl/WptiOIEQ1gnL29mX6/tR1tmlkYj8W4X+CSjWcUDGY1NpS/C7hSKqiMLM/l2QmSWZ73Ddz+gio8BCENYPQ46qnkzwXUbqvBkxjUQsWfZFgbuo3rAf+wN7jOO90+ynx4Pi3L+0nYL1SchDUgAP4gPV/7Id1q+1HShmuGkIqWRPgyxMFqP8HfjTnjXwY5bQfbJct6OIzKgMHotF/He1egsaxHSqG6wfdmQ5x8NyTFFqBcp2iSowHR3yk5+36hF7vXAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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" + }, + "e416201b-afeb-41ca-a03d-2281c28322aa": { + "name": "ATKey.Pro CTAP2.1", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAAA9CAIAAADAuAeYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAABGuSURBVHhe7ZwJfBPV9sczS/Y03Rco3XcKBVwRBHkiT58LqxvCE3AtoIICBQTZ2gItUigtVGihKPoXAR+yyPLhARZZ1EdVoPoQpKW0BVq6Zc9klvxPMrfQliZNl7QPP/l+LnTmnDuTyfzuvefcm0kws9kscHEvg6O/Lu5ZUC/8z4BnNL8WYYSIt3Y7HGsIeH5M/M4stO/CNkjCswPHan/5HRf/r0jI6gz+45/t/fkatO/CNmggxXhwHLbMNGM20d1TaEaAWy4DwzG4Ev7aXNgH9cLCR8ZBL8TEIjPHyWLCcYLo+jQHpDPTtO7iFUxIcnqD38vP9t6WgXwubNNUQpFQQDODq88Schnv7mKoylunAu4nlZ4uCR2neUYKirJaPdrpcjidAW25cJgWJxVdPYQ2ohtf+l7FNS+85+lMCVmDkTOZOBPF0TSHbC6cTqdJqP/vle9k0af8Hjrp++BJZT+mph45XDiZTpPwYuICAhebWVbAmVmjoWSxa1beRXSOhPristoTx3GFDCMIjMAJhdv1TdtpjRa5XTiTzpHw8rSFBOmBYRirN3IUIyAwAU2XLs5EbhfOpBMkNJTdqD58hJBKYELpN/455cN9zRRNKOTlG75g9K55ntPpBAkvTV9MkAoBJmBYTVTWorDUObSxDoZTjjJeS3Z91OB0OiQhzMMN16uq9x3CZVJOb/AZMUKodPMYfL8iKp6jaFIuL1+/jaNMqLYL59AhCTGB4MrMFIIQwzbNqGJyV/D2yDULGGM9dETIaErTN/JGF06iQxJSlbeqdu63dEGD0XvIMGlIIG/3eeZvssgYmOALZfKyNfkczfB2F86gQxJeSUrDcEIAiSitjtmYiqxWIlfOZQxqgZBg62rL1my22lzrn06h/RJS1bVVn+8l5FLOSHkMHCSPi0QOK77jnpKFRppNDC5TlGVsZs2cddx10fm0X8KShRlmM2vpgib17SjYmLC0JMagwUjCVHmrYt1nyOqis2mnhHS96mb+LkIuMzOMcsADsqhQqqoaQuPtYrpV6/X4I9KgYAHLEVJZ+apc1zDqJNopYcmSdWYTDTknRpLG4rKTnv1/CB7yQ8jQ2+VM0OAzIY8yKq2AwHEhaaiouL7pS3Swi06lPRIyWv3N3O3WhzMsz0yZIc6RJCYSNi8EASkMVIBapFR+bcUn6HgXnUrzZ2egbz1SekLk78u7W+TSe0uvZX1Ckm5oH4HhMgnIBVsgKmegmqWgNFPXOyczMPEVtN8ShuLSMxFD7n52JjdvS0HBCYlYrKeopYsWRkU1SZ2akZyS+uefxUJSCNdSr6p/8IEH5ibNrqmpfStxuqe7u9FkHDjw4XemTd29Z++Or3bI5Qo7mbKJNvVLSJg1a2ZxcfGsOfO8Pb04M0eQRO7GHFTDNnq94d0ZM+FO4BheW1+/MSfb19feXW03JPrrMGaW5erUPV56wdrJGoC+JiKrvtwvEAlBQFws9h33pOWJwkZ3hzPRhj+uoJ02cuHChf3fHpDL5VqdbuZ77yBrSyTNnb8pb7NcJocrUqnU8fFxu3ZsBztFGffs3Rvg76/T6iRiCVj+vHxl7/4Dnh4eZtsaGg1GygRtURAeHn6hqEij1pAkWa9SjRk9+ul/PMnXscXWrZ/u3Pm1m9LNaKDuG9DfSfoBbZYQlIvdthrtNOVG/g5S5G5mWDLQIy5/FbJ2BiKxWCqXQWEFHMRWZL2LufPm5+bn+/j6gn5wo/sPSPj+u2O8C7qCVGo5A2c2w9nAIhTC6G6x2JEQw3GRxKI3kJaaMuXtRH8Pd5wkl6eltSohtCRPH2+RUKjRaFNSliCrE2hbLKQp09Xl60tXbLianFX+yd3pScO9YFm0YQWspatyr6Zml8KxGVts3rCOMW/+wo15+d5e3tb+p4qLir6tX4vo9LqayltVllJtp6jrVXz9cc+PVcjkLMeKxaLffv+9sLCQt7fI9q92lJVXCIVCiqL6D+j38EMPIYcTaJuEFRn5lxYsvvLhqouL5pEyS1t2BAiPdFXNHws/urJg1aVZc27tOYIcnceChR/lbMr18bHqp1ZHhoefKDiKfDaY9f7M2pqbZSWXym2XqhulX2zbig6AV5k3R1WngpdQSGXJKSuRtSXWZa9XKOTwxuvqVR8mzUFW59AGCSEKlmfkSWQBhETqHv5gwKtjkcMBwlLel7gFEQo3kcjvqvWj4E7si/MXfJSVs9HX1wdurlqtjouOPn2yAPlsI5FIPD09le7udoqHh4dCoUAHCATTp0/DMYzjOJFEeurMqeLiEuRoysFDhy/+cVkoEtE0HR0R8dRTrQy5HaQNEpZnfWaqrhIICcaoDkttU8syE2Jx0MwprFaNSUTac+dqDp3orNW2JUuTczZu8rPGP7VaA8lqwfF/I1+LYB1qPW++8ZpGq8NxTCgUp6V/jKxNWbs2SyaXwfVAPJ71wQxkdRoOS8iZyz7OJaQKs4mRBocFvPwMsjuERa+g2a8TCqWA4wiRvLMejlqyNGVt9nofH0v/02g08bGxJ+3GPwtm69W0l6SkOSajEWZikBvtP3CgtrYGORo4feaHs7/+AvMfhmEC/QNeGf8ycjgNRyUsz/vSWFGOCUnaoA5b0p6WJVQqA6e+wmo1mESs+qmw9vgZ5Ggvy9PSIeT4eFviH6T70VFRR44cRD7bgH4dkdDDXTl2zCiY8+E4TjPsuqwNyNHA2rWZoB8/JCQmvoWszsQhCSG/LFu50dIFaUYaGNRjyvPI0UaCkt7GYSoNHVEo4yNiO8AJyzUvX5m+Kn21l7cXTEmh//WOiz125JCd+cZtYBTlB9Kqqqpfz50v+u13O+X8+aKSq80D3sL583RaLXRESFi2/d+XEPCQQyAoKvr9u+9PSqVSlmXdPZSvTZmMHM7EIQmrtn6tLymB4Z81aEI+nIasbUfs49VzygssxBKpuP770/WnLXl5myITZBNKN7fs9TnpqzO8fX1APxNFxcfFHT64HybdqJJj5OZtGTDggUFDhw0aYrPcP3DQjPdnowMaCI8If2zoECNF4QShUqnzNm9BDoEgMysLjPyo/uqECfIu+YKYQ822dHmOUCI3M4w4oGfPt+2tkLVK0PxEHCbLHIeT0pJFa5HVYWRSacrytOQVK72t46fAbGYoU+7GHJiBoRqt0jCMKuQKH39/fz8/+GerBPj7QVaKDmjEgg/nqVUqzCyQK2Sb8pCEpdeuHThwSC6TQcoqkYindckoCrQuYeX2/frLlwUiEavXBs15gx/H2ge0BklPf/+JY1itHpdJ6o6eUJ0tcjwyWTTD8CPHjrkpFNAdeQtGEnOS5vMVHKKh1xuNhrq6OlV9fX1dnZ2i17XwQPPDDz2Y0LcPRZuEpLC8vGL3N9+AEcYGmmUgRmp1urGjR/n5+fGVnU3ry9w/9n3K+Oc1DOKMTDqw7CRpXZ1qkWNYCKn0gHgp7uU/8JLNzNBQWvFj9HBcJOSMlOcTg/sdzEcO28vcs5PmffHl9sZTNJPJRJtoyN1Bxprq6pRlS6ZPTUS+lrh542ZUXN+AHv56rW7UqJEbsjNPnjp17Ph3MDtENVqCppnIiPCXXnwB7Tdiz779r05+3c/P12g0xsXE7Nvzr9j4BMtXzDFMr9OdPHEsIjwCVXUyrcSP6/m76otOkQIvRqCOmZ9sRz/ALGAt39NnoDRZYGuGNCTQ78Wnb37+L0Iqu3XosOb8RbeEWORzDK1W2yc+ftjQIZmZ2UovD08vr2Upy0cMHx4dHYVq2OZ26H108GAoaKftjHru2eBegRqdXiwWXy4uHj9xEs0wkMjAtT054gk7+jEMu/2rrwICAmBI0Wg1JpoOCw3pl9BPJHI4FjTF3qgI7xb6ZUxKWlT6gtjlK3rOfB05bCD08hX6+wgDfElfL2SyQcjiGeLAQKG/r8SvV1nGnXTAEeAeBQf12v/N1xCQ+t3Xz6DXwwAhEgqnvN5Fsec2774zXaW2rLcROFb488+gHwxpDM3MnPEuqtESJGn5HYORY55/dvSYc+fOUxQ1aswLUbG9YUhANdoKnA44O3Dsd+LYAre+8D91s4o3QljmNxyhWVXHj4RXuV1Zf+XqUUFQgTLhOBn128T3kdVsnjVnbkCvkMjY+KCwyEGPPgZvm7eXlpUFBoeFRcZExMZ7+/VY8NFi3n43N67fULj7wBl69AqdOv09ZO0Y0IFCw6PComIjY3tHxMTDyQNDwkeNGYfcdomK66P08r106RJsnzx1WqrwCI+MNRgsiwZtxV4vtKQPDtOsapuSFAcrw+VC/FuXmSESod/HCe7VKzV5aX29Cnwenp7Z2Rt++s9Z3tUFCEnytSmTNCoNbFuzYzNo8MFMx9c9MMpo+TAyNjbGTeEGg2p5RTnvqKyqgv9rqmsqypEFKDz787Lk1G2ffwF5ADJZaUnC2+Gi62n1pTEzhjW55kmv/nPE8L/pNFpoCR5enhP+OQk5bNGxNdJmvPfuOxKZGMYR2IY727dvn6FDh/Au+6BrsLZevV5nNBkJgoQZTlb2+lDo1PH9Pv1sG/xNGPAQTDGhDnTuF1+Z8NLLL3762RdePgGNW2oLElp+tqe7aO2l4Z3DyIt2Gsjfslkmk9E0DbNDlUrTSlDs2BppM9zd3UNDQlnWEgogSM98dzpytAZcA8jHT2cXLlisrq2bNHGCm5sbxNeQ4F6EULh9567nnntu0KCHwThn3od7v9m7Oj0tJipqS94nQrF45Og7HxM1l9AMN9Fu2ulUMMsI2eY7LJNJczZkq1QquI/u7sodu3btP2BzsdRy79BmJ3D06PFz5y+AEtCAIsMjRo8aiRwOIJfLZ8+bHx0bf/HS5d27v165Ej0Ob2mOFJW1ZvVn+Xn79uxmaPrbAweU3l49A3uCNzg42MfbS6XWnDmDFpmbTipgkCLIH8MfE9zV0rsCGOLg9d2U/DNUbeLvI4ZPGP/Sjl27QULI1ye/9sa1kssyaQvrW5Z+bN1Yty47dWU61LfutYyRMj4+bNjWLXlo/y5WpKd7KJVmgaULLl20EFkdQ6fVZa/JCAkNQfsNQEOE9w9hld/V6Q0URYMFJqC8BaYxkARTDRGxSS+0JBY4xplojmG7odCs5QF+jGhfN8lelxkY4A/JKg5zDLF47LhWPuVhOY6GGQDL2ingpps+RNKYwsKff/zprEgqgXo9/QNenTgROVri0OHDGzbc+ZIXNFNoSTp9C7/SxLfg20keNLIe8L5MpqtXr/IWPajLsv0T+vO7SEKYj1uUo0yW37Jj2O4rcBkmuAyOsVwGf20AwzCQLJggiwev7R+Hy9+SB00bWivkiscLCrLX33lUEJq2CQ62nMMEZ7NYODPrAHyq0iIr0lYplW5wp7V63eTJk+wsPUIfhSY1fXpiQcEJZNGooYlUVlbyu43R6XQmFhrXna+DLVu8iMDwzMxs2D59+oeSPy/PTZrt4enOe9EC24WxibqiyzCR562OA2/A5h1tzWsHzkD5jBwetQYNTanLV36zd59UKoHhZfOmjQkJfXj73axavWbnrq8lUgm8r5qa2u+PHfX2sawzVFZVPv7EP7y9vYwGw99HjEhJXrJly9bsnE8UbncW7e4G+vSgRx5Z83E62m9EcXHJfQ8O9PH1AY2hw5wvPCtXyJGvJd6b8UHRb7/t27tbr9O++ea0G7cqhYQQJ7DRI0d+8P6decjSZckHDh3GCcLT3X3a1MRnn3mat//yy6/LV6ykGAYXYONffrHxmp9FQhCxodf+1YD7C+Mq2ulU3nhr6rcHDyoUCrVa/cZrk1OTlyFHl2OV0Npd2of9Yzty5v9lbt2qjo1PgGkoDNAmiir86UyXfS5xN5YW2pG7bP/Yv6R+wKqMNaSQxDEM8hEY67pRPwDFQheOYzAawyOiZdZPviD1OH3ieHh4OO/qFpwSJ/7awIQSkkkIsaDlsKFDulc/wNUL20yv0AiRSAQSqupVRw7t699/AHJ0E65e2DbSV62uKC2rq62/XnGjT5/4btcPcPXCtnHu3HmaoaELMgwbFhrivK+cOY5Lwnse10B6jyMQ/D/exLg8R/4sQAAAAABJRU5ErkJggg==" + }, + "cfcb13a2-244f-4b36-9077-82b79d6a7de7": { + "name": "USB/NFC Passcode Authenticator", + "icon_light": null, + "icon_dark": null + }, + "91ad6b93-264b-4987-8737-3a690cad6917": { + "name": "Token Ring FIDO2 Authenticator", + "icon_light": null, + "icon_dark": null + }, + "9f77e279-a6e2-4d58-b700-31e5943c6a98": { + "name": "Hyper FIDO Pro", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "0bb43545-fd2c-4185-87dd-feb0b2916ace": { + "name": "Security Key NFC by Yubico - Enterprise Edition", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "73402251-f2a8-4f03-873e-3cb6db604b03": { + "name": "uTrust FIDO2 Security Key", + "icon_light": "data:image/png;base64,wolQTkcNChoKAAAADUlIRFIAAABkAAAADggGAAAAw5nCjcK5aAAAAAFzUkdCAMKuw44cw6kAAAAEZ0FNQQAAwrHCjwvDvGEFAAAACXBIWXMAABJ0AAASdAHDnmYfeAAAB8OfaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49IsOvwrvCvyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0OCA3OS4xNjQwMzYsIDIwMTkvMDgvMTMtMDE6MDY6NTcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjAtMDQtMTBUMTE6NDY6MTYtMDQ6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUyM2FkMzNkLTkwMjMtNGNlNS05MGJmLWUzZmExZDdjMGFlNiIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjBhMTFlZTdmLWQ5ZTQtYWM0NC1hM2I2LTllZmVkYTA0NDA5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmI4ZGRmYTA5LTdiM2MtNDMwMy1iNTlmLWE2MTQyZTdiMTJhYSI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YjhkZGZhMDktN2IzYy00MzAzLWI1OWYtYTYxNDJlN2IxMmFhIiBzdEV2dDp3aGVuPSIyMDIwLTA0LTEwVDExOjQ2OjE2LTA0OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MjNhZDMzZC05MDIzLTRjZTUtOTBiZi1lM2ZhMWQ3YzBhZTYiIHN0RXZ0OndoZW49IjIwMjAtMDQtMTBUMTE6NDY6MzItMDQ6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8eG1wTU06SW5ncmVkaWVudHM+IDxyZGY6QmFnPiA8cmRmOmxpIHN0UmVmOmxpbmtGb3JtPSJSZWZlcmVuY2VT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", + "icon_dark": "data:image/png;base64,wolQTkcNChoKAAAADUlIRFIAAABkAAAADggGAAAAw5nCjcK5aAAAAAFzUkdCAMKuw44cw6kAAAAEZ0FNQQAAwrHCjwvDvGEFAAAACXBIWXMAABJ0AAASdAHDnmYfeAAAB8OfaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49IsOvwrvCvyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0OCA3OS4xNjQwMzYsIDIwMTkvMDgvMTMtMDE6MDY6NTcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjAtMDQtMTBUMTE6NDY6MTYtMDQ6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUyM2FkMzNkLTkwMjMtNGNlNS05MGJmLWUzZmExZDdjMGFlNiIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjBhMTFlZTdmLWQ5ZTQtYWM0NC1hM2I2LTllZmVkYTA0NDA5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmI4ZGRmYTA5LTdiM2MtNDMwMy1iNTlmLWE2MTQyZTdiMTJhYSI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YjhkZGZhMDktN2IzYy00MzAzLWI1OWYtYTYxNDJlN2IxMmFhIiBzdEV2dDp3aGVuPSIyMDIwLTA0LTEwVDExOjQ2OjE2LTA0OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MjNhZDMzZC05MDIzLTRjZTUtOTBiZi1lM2ZhMWQ3YzBhZTYiIHN0RXZ0OndoZW49IjIwMjAtMDQtMTBUMTE6NDY6MzItMDQ6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8eG1wTU06SW5ncmVkaWVudHM+IDxyZGY6QmFnPiA8cmRmOmxpIHN0UmVmOmxpbmtGb3JtPSJSZWZlcmVuY2VT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" + }, + "c1f9a0bc-1dd2-404a-b27f-8e29047a43fd": { + "name": "YubiKey 5 FIPS Series with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "504d7149-4e4c-3841-4555-55445a677357": { + "name": "WiSECURE AuthTron USB FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "5fdb81b8-53f0-4967-a881-f5ec26fe4d18": { + "name": "VinCSS FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "2d3bec26-15ee-4f5d-88b2-53622490270b": { + "name": "HID Crescendo Key V2", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "cb69481e-8ff7-4039-93ec-0a2729a154a8": { + "name": "YubiKey 5 Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "0076631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "HYPR FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACNgAAAjYCAYAAAAADILPAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABANpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjVEMjA4OTI0OTNCRkRCMTE5MTRBODU5MEQzMTUwOEM4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ4RThERjcwNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ4RThERjZGNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTBhMjJkMGUtMjUzNy00ZjU1LWEzNTctZjE3Yzk0Y2ZlNTkxIiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OTg5YTAzY2YtNjlhZS0xZDQwLWI0OWYtOWQxMTFlMGU2YjM1Ii8+IDxkYzp0aXRsZT4gPHJkZjpBbHQ+IDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCI+UHJpbnQ8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl2Dyx0AAJydSURBVHja7N3/bVxVGoDhE0QBKWEaQEoJLiEdrDvYNICSiAIQFZBUsNkKGCrAiAIYKiBbgXcOMxP/UPISknjGnnke6Uj2hD/gs3WUe+/LuY8uLy8HAAAAAAAAAADwfl8ZAQAAAAAAAAAAfJjABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAMLXRgDsfPPk21P8z36yXhd++gAAACdtXhs+Ngb2aGkE8LcW2wV3ZbVdAH/rt4vvDAEQ2AAnad44/dd6Pd1epD8yEgAAgJM2rw+fGwMH9nbc/J+AVuv1x7WvV9uvL7b/LBy7s/X60Rg4gNv77Pz+f+/5s6VRAcBpeXR5eWkKwF+O/ASb21HNjb3QTx8AAOCkzWvGX4yBB2a1XfNB76/jKtDZfQ7H4PfhFBvuv110M/feP659L4iEI+IEG2Bygg1wzCqqAQAAgJ1dlODakYdkce139ukHfq/ng92fx1V0szQ2HpiXwyk23H9P/ubPl+NmfLMaN08sAwAeCIENcIwXM6IaAAAA/qk36/XMGDgiuwe+Z7c+X43Ng9156s1yOPGG++3V2LzCb2EUPGBnH/h8twf/ut2Xl0YFAPebwAY4BqIaAAAAPtc85UNgwylYbNe8j/J8+9k86WY5rqIbrzXhPnl97XcVjsnZez67GFcRpOgGAO6ZR5eXl6YA/OWbJ98+pH/dLxnVPPLTBwAAYO3P9XpsDPCX3UPeGZ8th1NuOJy5L/9uf+aELbd78S64EUDCAfx28Z0hAE6wAR4UJ9UAAABwl5bba05gcx9mrvPt96tx9ZB3OQQ37M+MCX4YTrHhdJ2Nm6fdXNzajwU3ALAnTrAB3rmnJ9jsI6pxgg0AAADT+Xr9aAzwUVbr9WZ4wMt+OMUGPmy53YvnnnxhHHA3nGADTAIb4J17FNgs1uvfY38n1QhsAAAAmOaD2z+NAT7Jcr3+OzYPeFfGwR2YAeS5MUB6O67ixzdD/AhfjMAGmAQ2wDsHDmwWYxPUzNNqnux7L/TTBwAAYOuXA1yXwrFZjc2D3dfDaQp8OYuxOcUG+HgX271Y/AifSWADTF8ZAXDgi+JnY3Pzcl4cfz/cxAQAAOCwXhsBfLbFuHnPZ5488tRY+Eyr9XplDPCPzPvt32/34rknvxjuwQPAJxPYAPu2GKIaAAAA7q+lEcAXtRib1/r8Z2xewTZjmzNj4RO9NAL4ZPM+/PNxdW/+2XaPBgA+ksAG2IfFENUAAADwMMxXKayMAe7E47GJbX4a7hHxaeb+/MYY4LMtxs2TbZ5t92gAIAhsgLv8C7qoBgAAgIfIw1u4e4txde/Iw13+iR+MAL6o3Wuk5ilj87SxcyMBgPcT2ABf0mKIagAAAHj4fjYC2KvbD3efGglhObzOD+7K3H9/3O7H7u8DwC0CG+BzLYaoBgAAgOMyT7B5awxwEPPh7oxs5n2mF2Nz7wlue2kEcKfmiWLXTxk7H04ZAwCBDfBJFkNUAwAAwHHzmig4rMV6PR+be0/zNIUzI+Ga5XpdGAPsxZPtPrx7FrAwEgBOlcAG+FjzL82iGgAAgP1fi3EYXhMF98f5ev00Nvek5tdOUWD6wQhgr3an2sy9eJ40dmYkAJwagQ1QFkNUAwAAcCjn22sxD5IPwwk2cP8sxtUpCi+GCPHUvVqvlTHAQczX+V0PHwHgJAhsgNsWQ1QDAABwaOdj8xB5emocB/F2bF5BAtw/Mzy8/vqohZGcrJdGAAe12O7Df45N+CgMB+CoCWyA63bHO4pq4P/s3e9120i64OF375nvo41gcCNodQRmR9B2BEZHYDsCSxHYjsDoCKyOoNkRtCaC4WSgDHZZJmn9lygSBRRQz3MOju/OfuJLNQS4fq4CAIDxtHEd1ySvjGQ0fxgBTOKe6biSenVhFxsowc3wMa0vNEYCwBwJbIC7D8EAAACMp43bcU1iB5vxOCYKpmN3XEm6FsZRld+NAIqR1hh2/5DXDmMAzI7ABgAAAKAMbdyPa5K0ULEwnlGsws4IMDXpfim0qcvn2BzrB5T3bCu0AWBWBDYAAAAA42vj4bhm51cjGo1dbGCaFiG0qUWKa74YAxT9nCu0AWAWBDYAAAAA42rj6bgmcUzUeP4wApi0RQhtamAXG5jGM6/QBoBJE9gAAAAAjKeN5+OapAkLEWNZhkVbmINFbCKbb+6ns5Tu03Ycg+k8//69vs5icxQqAEyGwAYAAABgHG3sF9fs2MVmPBZtYT7SvXS3g4KF3Xk5NwKYjHT//bi9H58ZBwBTIbABAAAAGF4bL4trkrfGNhrHRME878O7hV2hzTys1ldnDDApN0Ob1jgAKJ3ABgAAAGBYbbw8rklOwyLwWJZGALO0W9j9OyzszoVdbGCamu3zcTrKb2EcAJRKYAMAAAAwnDYOi2t2HBM1jqtwTBTMWRPXC7unxjFpK/drmLTF9l7sGD8AiiSwAQAAABhGG8fFNcmvxjiav4wAZm8Rm91sLOxO2xcjgFk8N6djo94bBQAlEdgAAAAA5NfG8XFNsjDK0dgRAeq6Z/8nHBs1VctwtB/MQQodP8UmfPQMDEARBDYAAAAAebXRT1yTpIUGx0SNY7W+Lo0BqnESjo2asnMjgNk4DcdGAVAIgQ0AAABAPm30F9fsvDLW0SyNAKqziM3uCWdhYXdq92tRJMzvuTrtLiY2B2A0AhsAAACAPNroP65JLCqM53cjgGp9DMeUTM0XI4DZSaHjt9jsaNMYBwBDE9gAAAAA9K+NPHFN0oTjSsaSdkO4MgaoVrr/pkXdT2E3mynoYnO8HzA/i9hEj++NAoAhCWwAAAAA+tVGvrhmZ2HMo7kwAqheWtC1m800nBsBzFYKHVPwaDcbAAYjsAEAAADoTxv545rkrVGP5g8jAMJuNlPRhV1sYO4WYTcbAAYisAEAAADoRxvDxDVJOiLKgu44lkYA3LDbzcbRfeX63Qhg9m7uZuMZGYBsBDYAAAAAx2tjuLhm57Wxj+IqHBMF3NbEJrI5M4oifd7eu4H5W6yv/3hOBiAXgQ0AAADAcdoYPq5JfjX60TgmCnjIx9iENo1RFCXFNV+MAaqRdrD5Fo7wAyADgQ0AAADA4doYJ65J/Mvc8SyNAHhEOirq7+3vB8phFxuoTzrC789whB8APRLYAAAAABymjfHimh2RzThW6+vSGIBHnGx/P3wNuyeUwvF+UKdd9PjeKADog8AGAAAA4OXaGD+uSRwTNR7HRAH7/K6we0I5zo0AqpWOi0rHRokeATiKwAYAAADgZdooI65JFr6O0dgJAdhHimv+DEdGlWC1vjpjgGqlnR//DtEjAEcQ2AAAAADsr41y4pqkCYsEY0lHRK2MAdjDzSOjGJddbKBu6dlZ9AjAwQQ2AAAAAPtpo8zF0de+mtEsjQB44e+RtHuCI0rGswo7kEHtRI8AHExgAwAAAPC8Nsr9S/hffT2j+cMIgBdKu479JxzxN6YvRgCE6BGAAwhsAAAAAJ7WRtn/wjUt1ja+plHYBQE4RFrMdUTJeJZhBzLg+jn6P+HIVQD2JLABAAAAeFwb09g+fuGrGo3IBjiUI0rGc24EwFaKHv8O0SMAexDYAAAAADysjeksfDomajyOiQKO/V3jiJLhLdfXpTEAN6Tn/k/GAMBTBDYAAAAA97UxrV0FXofF2bEsjQA4Ujqa5M9wRMnQvhgBcMf79fXNczUAjxHYAAAAANzWxjSP7Fj46kaxCrsgAMcT2Qyv297DAW56vb0fi2wAuEdgAwAAAHCtjWnGNYljosbzuxEAPUiLuX9vfxcxjHMjAB6QYsf/hOgRgDsENgAAAAAbbUw3rkle+wpHszQCoEfpd9F7YxhEF3axAR6Wokc7iwFwi8AGAAAAYPpxTZIWASwAjCMdEbUyBqBHn2bwe2kq7EIGPPV8bWcxAH4Q2AAAAAC1a2M+i5hvfZ2juTACwO+nSfq8vq6MAXjC1xDZABACGwAAAKBubcxr8XLhKx3NX0YAZPo9lXZPODGKbFJc88UYgGekd4ZPxgBQN4ENAAAAUKs25rczQDoiqvHVjiLtYGMHBCDXvf3PENnkZBcbYB/vw85iAFUT2AAAAAA1amO+fzn+2tc7mqURAJmIbPJKcY2j/oDa3yMAeIbABgAAAKhNG/P+S/FXvuLR/GEEQEa7yKYxiizOjQDwPgHAU/5hBAAAAEBF2pj/X4anHWzSDgeOuhhe2v1gZQw8YPHI//6vuB1LLIyKZ6TI5u/19cv6ujSOXqX7989hl6A5O9n+N/SQm4FyE0I29nuvSD9Tv3nuBqiHwAYAAACoRRv1/EvTFNl0vvLBpcWVpTHwgJf+XNxcBF5s/3x15/9NvdLPR9rJRmTTP/Ocv5ceBXa6/W9ud1/+5/bPJkQ4bJ65m+39WGQDUAGBDQAAAFCDNuraxj0txHe+dpism7HW8oH//2Z7pUXef23/XBhbVUQ2MIyb/309FOfsApzFjfvxqbFV5fTG/VhkAzBzAhsAAABg7tqoK65J0r+m/c1XD7O12l7LO/97E9eLuym0WxjVrIlsYHy7//bu3o939+Kftvdi0c28iWwAKiGwAQAAAOasjfrimmT3L6mXfgSgKqvtdXOXhd3uNrtF3saYZne/F9lAeS4f+G9yced+fGJMsyKyAaiAwAYAAACYqzbqjGt2fg2BDXB/kbeJzcLur2GBdy52kc3/hkVdKNnyzrPZ6Z37MdOXvtNPYSdJgNn6HyMAAAAAZqiNuuOa5LUfA+ABq/XVra836+v/rq+f19d52P1k6naRjWAKpiPddz/HZseT/7O9L3/e3qfxHgJAgQQ2AAAAwNy04S+1kyYcBQM8Ly3wnsUmtEk7oKR/dX9hLJO0O55EZAPTlO69H7b34p+3/7f40fsIAAUR2AAAAABz0oa/zL7JLjbAS6zi9u42YpvpEdnAPOx2t9nFj2Ib7yUAFEBgAwAAAMxFG/4S+65fjQA40FXcj22WxjIJu8gGmIdV3I5tzsMxUlN6PzkzBoD5ENgAAAAAc9CGuOYhi7CLAXC8XWzzS1jcnYpTvxdhltK99yyuj5HqtvdoyvVx+64CwAwIbAAAAICpa8Mi4lMcEwX0aRXXi7tpdxtHSPn9CIwjHRn12/Z+/Fs4QqpkX0NkAzALAhsAAABgytqwePicV0YAZJLimhTZ2NWm7N+T740BZm23y9jPcb2rDeX5FJvdxQCYMIENAAAAMFVtiGv2YQcbILdVXO9qYxeF8nwKOydALXa72vzfED6WJh3b+meIbAAmTWADAAAATFEb4pp9pb/MF9kAQ+lis4PCL+traRzFSL8zLepCPdKuNmdxHT6ujKSY5/Kv2z8BmCCBDQAAADA1bYhrXsoxUcDQlrGJbNLibmccRbBzAtSp296L34TwsQSn2/sxABMksAEAAACmpA1xzSHsYAOMZRWb3ROENuOzcwLU7SI24aMdxsZ36p0GYJoENgAAAMBUtOEvog/VhF0LgHGtQmhTgvS74JsxQNWWIbQp5d3mzBgApkVgAwAAAExBG+KaYy2MACjAKq5DmwvjGO33gd+pwDKuQ5uVcYziY9hpEmBSBDYAAABA6dqwENiHt0YAFGS1vt6EHRTG/N3aGgOwvQen6PG3ENqMIb3n2GkSYCIENgAAAEDJ2hDX9CX9xf2JMQCFWcYmskmxzco4BmVRF7ipW18/r6/z9XVlHINJz+ffPKcDTIPABgAAAChVG+KavtmCHihVOi4q7aBgYXdYf4ZFXeBauv+ebe/HnXEMpolNZANA4QQ2AAAAQInaENfk8KsRAIU7i80OChdGMYgU1/xpDMAdKbRJR0alHcYujWMQi/X1yRgAyiawAQAAAErThrgmF7sUAFOwis2RUb+EY6OGkI6JsqgLPGQZm+jxQ9hdbAjvw46TAEUT2AAAAAAlaUNck0v618dvjAGYkGVcHxtFXu+3v4MBHvJ5ez+2u1h+6V2oMQaAMglsAAAAgFK0Ia7JJcU1aScI//IYmKKz2OygsDSKrNIuNqfGADwiPUe+2V6eKfNJO05+MwaAMglsAAAAgBK0Ia7JRVwDzOle5piSfE62v4sdJwg8Je1iYzebvBzdB1AogQ0AAAAwtjbENbmIa4C5SceU/Ly9v9E/i7rAPuxmk186uu+1MQCURWADAAAAjKkNcU0u4hpgrlaxiWzOjSLb72aLusA+7GaTV3pPaowBoBwCGwAAAGAsbYhrchHXADU4i01oszKK3lnUBfa1283mN8+evUtH9n0zBoByCGwAAACAMbQhrslFXAPUds9LkU1nFL068XsaeKFu+wzqCL9+paP7zowBoAwCGwAAAGBobVi0y0VcA9Qo3fN+C7sn9G0RFnWBlz+Lpujxs1H06uP2ngzAyAQ2AAAAwJDaENfkIq4BateF3RP6lhZ1T40BeKEPsTk2ynNpf9I71IkxAIxLYAMAAAAMpQ1xTS7iGoDb98MLo+iNRV3gEBcheuxTE5voEYARCWwAAACAIbQhrslFXANwW7ofpp0TPhhFL9IONhZ1gWOeU0WP/Xi/vl4bA8B4BDYAAABAbm2Ia3IR1wA87rN7ZG/Sou7CGIAD7KLHc6PohV3FAEYksAEAAAByakNck4u4BuB5y3BESV8s6gLHOItNaOPZ9Tgn3q8AxiOwAQAAAHJpw1/+5iKuAXj5PXNpFEdpwlFRwHEuPMP24nU4KgpgFAIbAAAAIIc2xDW5iGsAXu5qe+/sjOIojooC+niW/d+ws9ixPoVdxQAGJ7ABAAAA+taGuCaXbn39HOIagEP9tr7OjeEofscDx9pFjxdGcbAm7CoGMDiBDQAAANCnNiy85dLFZmEYgOOcuZ8epdnOEOAYKbJ5E3YWO4ZdxQAGJrABAAAA+tKGuCaXLiwGA+S4r9oR7DBp14TGGIAepHvxZ2M4mPcvgAEJbAAAAIA+tOEvd3PpQlwDkOv+mo4oEdkcxu99oC8fPO8erAm7igEMRmADAAAAHKsNi2y5dGGxASCnyxDZHGqxfQYA8Nw7rndhVzGAQQhsAAAAgGO0Ia7JpQuLDABDENkc7tP6OjEGwPPvqE68kwEMQ2ADAAAAHKoNf5GbSxcWFwCGJLI5TFrU/WgMgOfg0S3W12tjAMhLYAMAAAAcog1xTS5dWFQAGIPI5jDv19epMQCeh0f3yQgA8hLYAAAAAC/Vhrgmly4sJgCMSWRzGIu6gOfi8TXr68wYAPIR2AAAAAAv0Ya4JpcuLCIAlEBk83KLcDQJ4Pm4BO9iE9oAkIHABgAAANhXG+KaXLqweABQEpHNy9nFBvCcPL6T9fXRGADyENgAAAAA+2hDXJNLFxYNAEoksnmZJhxNAnheLuXd7dQYAPonsAEAAACe04a4JpcuLBYAlCxFNh+MYW/paJITYwAyPTd3xrA3u4oBZCCwAQAAAJ7Shrgmly7ENQDu1/PiaBIgp99CZLOvxfYCoEcCGwAAAOAxbYhrcunCYi3A1O7b58awl/exOS4KIIf0DL00hr14lwPomcAGAAAAeEgb/kI2ly7ENQBTdBZ2TtiXXWyAnN7E5gg/ntZs3+sA6InABgAAALirDXFNLl2IawCmzM4J+z9LnBoDkMnV+vpl+ydPEzwC9EhgAwAAANzUhrgmly7ENQBzYOeE/XwyAiAjkc1+mtgc3QdADwQ2AAAAwE4b4ppcuhDXAMzF1faeblH3aYvtBZBLih0/GMOz0i42J8YAcDyBDQAAAJC0Ia7JpQtxDcDcpEXdN8bwLEeTAEM8a58bw5NSXGMXG4AeCGwAAACANsQ1uXQhrgGYq2XYOeE5i7CLDZDf2fq6MIYnvQu72AAcTWADAAAAdWtDXJNLF+IagLn7HBZ1n2MXG2AI6bl7ZQyPsosNQA8ENgAAAFCvNsQ1uXQhrgGoRbrfXxrDoxZhFxsgv6vYHN13ZRSPsosNwJEENgAAAFCnNsQ1uXQhrgGoydX2vm9R93F2sQGGkGJHR/c97mT7HgjAgQQ2AAAAUJ82xDW5dCGuAaiRRd2nLdZXYwzAQM/jnTE86p0RABxOYAMAAAB1aUNck0sX4hqA2n8PdMbwKLvYAENJwaOj+x7WhF1sAA4msAEAAIB6tCGuyaULcQ0Am0XdlTE8+hzSGAMwgCvP5k8SPAIcSGADAAAAdWhDXJNLF/4CH4CNtKj7xhgeZVEXGIqj+x7XrK/XxgDwcgIbAAAAmL82xDW5dCGuAeC2tKh7bgwPSgu6J8YADOTz+loaw4PeGQHAywlsAAAAYN7aENfk0oW4BoCHncUmtOG2FNe8NwZgQOl5/coY7llsLwBeQGADAAAA89WGuCaXLsQ1ADzNou7D3hoBMKCV53b3Y4C+CGwAAABgntoQ1+TShb+kB+B5jop6WLN9TgEYysX24v47Y2MMAPsT2AAAAMD8pKMXxDV5dCGuAWB/n8NRUQ95ZwTAwOwq9rDWCAD2J7ABAACAeUlhzSdjyKILcQ0AL+d3x32n2wtgKCmusavYfYJHgBcQ2AAAAMB8pLimNYYsurBACsBhHBX1MIu6wNDSrmJLY7jlxDskwP4ENgAAADAP4pp8uhDXAHCcs/W1MoZb0nPLiTEAA/Ncf99bIwDYj8AGAAAApk9ck08X/hIegH74fXKf5xdgaKuwq9hdi3BsH8BeBDYAAAAwbeKafLqwGApAf5br68IYbnFMFDCGdFTUyhjcjwFeSmADAAAA0yWuyacLcQ0A/fuwvq6M4YcmNjsnAAzpans/5tprIwB4nsAGAAAApklck08X4hoA8litry/GcMtbIwBGkHYUWxrDDyfeLwGeJ7ABAACA6RHX5NOFuAaAvM7C0SQ3pV0TTowBGIHn/tsEjwDPENgAAADAtIhr8unCX7IDMIxzI/ghxTWOJgHGsFpfn43hh0Vsju4D4BECGwAAAJgOcU0+XYhrABj2987SGH6wawIwlhQ8XhnDD943AZ4gsAEAAIBpENfk04W4BoDh2cXm2iLsmgCMI8U1X4zhB8EjwBMENgAAAFA+cU0+XYhrABjHMuxic5NjooCxpGOiVsbwXbO+To0B4GECGwAAACibuCafLsQ1AIzL76Fr74wAGEnaxcauYtfsYgPwCIENAAAAlEtck08XFjUBGN9q+zsJuyYA478frIzhO++gAI8Q2AAAAECZxDX5dCGuAaAcdk24ZtcEwP14fCfh2D6ABwlsAAAAoDzimny6ENcAUJZV2MVmx4IuMPa7wsoYvvvVCADuE9gAAABAWcQ1+XQhrgGgTHZN2GjCMVGA+3EJBI8ADxDYAAAAQDnENfl0Ia4BoFyrsIvNjmOigLHfG1bG4JgogIcIbAAAAKAM4pp8uhDXAFC+L0bwnQVdYGx2sdlwTBTAHQIbAAAAGJ+4Jp8uxDUATMPl+loag2OigNFdrK8rY4iFEQDcJrABAACAcYlr8ulCXAPAtNg1YWNhBMCIUlxjVzHBI8A9AhsAAAAYj7gmny7ENQBMz3J9rYwh3hoBMLLPRuB+DHCXwAYAAADGIa7JpwtxDQDTZRebzY4JJ8YAjOhq+15Ru4URAFwT2AAAAMDwxDX5dCGuAWD6v8uujCFeGwEwMsdEbYLHxhgANgQ2AAAAMCxxTT5diGsAmAeLuhG/GgEwssvYHN1Xu4URAGwIbAAAAGA44pp8uhDXADCv32u1WxgBUIDfjUDwCLAjsAEAAIBhiGvy6UJcA8C8rNbXReUzOAmRDVDGu0btx/a5FwNsCWwAAAAgP3FNPl2IawCYJ7smWNQFynnnqJngEWBLYAMAAAB5iWvy6UJcA8B8pR1sVpXPwLEkQAm+GIHABiAR2AAAAEA+4pp8uhDXADB/tR8TdRqbnRMAxrRaX8vKZyB4BAiBDQAAAOQirsmnC3ENAHWwa4JdE4Ay1H5sn+ARIAQ2AAAAkIO4Jp8uxDUA1GO1vi4rn8ErPwZAAS6MQPAIILABAACAfolr8ulCXANAfWrfNWHhRwAowNX2faRmgkegegIbAAAA6I+4Jp8uxDUA1Ps7sGaOJQFK8Yf7MUDdBDYAAADQD3FNPl2IawCoV9o1ofajSRZ+DIACXGzvye7FAJUS2AAAAMDxxDX5dCGuAQC7JgCUQfAIUDGBDQAAABxHXJNPF+IaAEhqX9B95UcAKETtwePCjwBQM4ENAAAAHE5ck08X4hoA2ElHkiwr/vwLPwJAIWo/JuonPwJAzQQ2AAAAcBhxTT5diGsA4C7HRAGUoeZdxRa+fqBmAhsAAAB4OXFNPl2IawDgIbUfE7XwIwAU4q+KP/vJ+mr8CAC1EtgAAADAy4hr8ulCXAMAj1mtr8uKP79jSYBS1B482lEMqJbABgAAAPYnrsmnC3ENADxnWfFnt6ALlOKq8vvxKz8CQK0ENgAAALAfcU0+XYhrAGAff1T82QU2gPux+zHAqAQ2AAAA8DxxTT5diGsAYF/L2OycUKuFHwGgoPtxrQQ2QLUENgAAAPA0cU0+XYhrAOCllhV/dou6QCkuo97g8WR9NX4EgBoJbAAAAOBx4pp8uhDXAMAh/qr4s//L1w8UZFnxZxc8AlUS2AAAAMDDxDX5dCGuAYBDLSv+7BZ0gZLUHDy6HwNVEtgAAADAfeKafLoQ1wDAMWo+lsSCLlCSZcWf/SdfP1AjgQ0AAADcJq7JpwtxDQD0YVnp5z5ZX42vHyhEzcGjezFQJYENAAAAXBPX5NOFuAYA+lLzsSSNrx8oyLLSz21HMaBKAhsAAADYENfk04W4BgD6dFnxZ7eoC5Sk5uDR/RiojsAGAAAAxDU5dSGuAYC+LSv+7P/y9QMFqTl4bHz9QG0ENgAAANROXJNPF+IaAMhlWenntmMC4F7sfgwwCoENAAAANRPX5NOFuAYAcqp114TGVw+4HxfBjmJAdQQ2AAAA1Epck08X4hoAyO3flX7uxlcPFEbwCFAJgQ0AAAA1Etfk04W4BgCGcFnxZ3csCVCSWoNH92KgOgIbAAAAaiOuyacLcQ0ADKXmwObE1w+4H7sXAwxNYAMAAEBNxDX5dCGuAYCh1bqoa9cEoCTLij+7+zFQFYENAAAAtRDX5NOFuAYAxrCq9HPbNQFwP3Y/BhicwAYAAIAaiGvy6UJcAwBj+Xeln/snXz1QmFWln9sONkBVBDYAAADMnbgmny7ENQAwplqPiLJjAlCav9yPAeZPYAMAAMCciWvy6UJcAwBjW1X6uRtfPVCYq0o/97989UBNBDYAAADMlbgmny7ENQBQglp3sGl89YD7sfsxwNAENgAAAMyRuCafLsQ1AFCSlREAuBcDkJ/ABgAAgLkR1+TThbgGAEqzqvRzL3z1gHuxezHAkAQ2AAAAzIm4Jp8uxDUAUKKVEQAU4dIIAOZNYAMAAMBciGvy6UJcAwCl+m+ln/vEVw8U5soIAOZNYAMAAMAciGvy6UJcAwAlq3VB99RXDxRmVennXvjqgVoIbAAAAJg6cU0+XYhrAKB0jiQBKMN/jQBg3gQ2AAAATJm4Jp8uxDUAAAAA8J3ABgAAgKkS1+TThbgGAKai1h1sfvLVA4VZVvq5G189UAuBDQAAAFMkrsmnC3ENAEzJVaWf+8RXD1CExgiAWghsAAAAmBpxTT5diGsAAAAOcWUEAPMmsAEAAGBKxDX5dCGuAYCpujQCAPdiAPIS2AAAADAV4pp8uhDXAMCU2TUBAAAyE9gAAAAwBeKafLoQ1wAA07MwAoAi/GQEQC0ENgAAAJROXJNPF+IaAACAvtS4o9iJrx2ohcAGAACAkolr8ulCXAMAc7EyAoAiXBoBwHwJbAAAACiVuCafLsQ1ADAn/zUCAADIS2ADAABAicQ1+XQhrgEAAACAFxHYAAAAUBpxTT5diGsAAAAA4MUENgAAAJREXJNPF+IaAAAAADiIwAYAAIBSiGvy6UJcAwAAAAAHE9gAAABQAnFNPl2IawAAAADgKAIbAAAAxiauyacLcQ0AAAAAHE1gAwAAwJjENfl0Ia4BAAAAgF4IbAAAABiLuCafLsQ1AAAAANAbgQ0AAABjENfk04W4BgAAAAB6JbABAABgaOKafLoQ1wAAAABA7wQ2AAAADElck08X4hoAAAAAyEJgAwAAwFDENfl0Ia4BAAAAgGwENgAAAAxBXJNPF+IaAKjdP40AAADyEtgAAACQm7gmny7ENQBAxKkRALgfA5CXwAYAAICcxDX5dCGuAQAAKMmJEQDMl8AGAACAXMQ1+XQhrgEA6nZpBABF+MsIgFoIbAAAAMhBXJNPF+IaAIArIwAAYEgCGwAAAPomrsmnC3ENAHDfwggARndqBADzJrABAACgT+KafLoQ1wAAAJTqxAgA5k1gAwAAQF/ENfl0Ia4BAACgPI7sA6ohsAEAAKAP4pp8uhDXAACPW1T6uf/y1QOFqfWIqEtfPVALgQ0AAADHEtfk04W4BgAAYAocEQUwcwIbAAAAjiGuyacLcQ0A8LzGCACK8E8jAJg3gQ0AAACHEtfk04W4BgDYT1Pp53YkCVAaR0QBzJzABgAAgEOIa/LpQlwDAOyv1h0Trnz1QGFqPSLK/RiohsAGAACAlxLX5NOFuAYAeJlTIwBwPwYgP4ENAAAALyGuyacLcQ0A8HJNpZ/bkSRASWrdvWbpqwdqIrABAABgX+KafLoQ1wAAh2kq/dyOJAFKYvcagAoIbAAAANiHuCafLsQ1AMBhal3QFdcApWkq/dwrXz1QE4ENAAAAzxHX5NOFuAYAOFytR5I4HgooTVPp5/6vrx6oicAGAACAp4hr8ulCXAMAHGdhBABF+KnSz21HMaAqAhsAAAAeI67JpwtxDQBwvH9V+rn/8tUDhWkq/dx2FAOqIrABAADgIeKafLoQ1wAA/WiMAKAIp0YAMH8CGwAAAO4S1+TThbgGAOjPotLPvfTVA+7F7scAQxPYAAAAcJO4Jp8uxDUAQH/slgBQhqbSz33lqwdqI7ABAABgR1yTTxfiGgCgX03Fn33p6wcK8lOln/vSVw/URmADAABAIq7JpwtxDQDQv1p3sLFjAuB+7H4MMAqBDQAAAOKafLoQ1wAAebyq9HPbMQEozaLSz/1vXz1QG4ENAABA3cQ1+XQhrgEA8llU+rlXvnqgIKcVf3bBI1AdgQ0AAEC9xDX5dCGuAQDyqXlB97++fqAgi4o/uyOigOoIbAAAAOokrsmnC3ENAJDXouLPvvT1AwX5yf0YoB4CGwAAgPqIa/LpQlwDAORX84LuytcPFGThXgxQD4ENAABAXcQ1+XQhrgEAhrGo+LOvfP1AIZrt5V4MUAmBDQAAQD3ENfl0Ia4BAIbRRL0LuktfP1CQRcWf/S9fP1AjgQ0AAEAdxDX5dCGuAQCGs6j4s698/UBBXrkfA9RFYAMAADB/4pp8uhDXAADDqnlB99++fqAgi4o/+6WvH6iRwAYAAGDexDX5dCGuAQCG97riz25BFyhFE/Ue1+d+DFTrH0YAAAAwW+KafFJY0xkDADCw0/V1UvHnt6ALlKLm2HHp6wdqZQcbAACAeRLX5COuAQDGsqj4s6/W15UfAaAQNR/XJ3YEqiWwAQAAmB9xTT7iGgBgTG8r/uwWdIGS1LyDzb99/UCtBDYAAADzIq7JR1wDAIwpHQ11WvHn/8uPAFCI15V/fsEjUC2BDQAAwHyIa/IR1wAAY7OgC1CGX92PAeoksAEAAJgHcU0+4hoAoAS1L+gu/QgAhVi4FwPUSWADAAAwfeKafMQ1AEApat7Bxm4JQCnSUX1NxZ/fcX1A1QQ2AAAA0yauyUdcAwCUovbjoZZ+BIBCvK388wsegaoJbAAAAKZLXJOPuAYAKEntx0PZMQEoheARoGICGwAAgGkS1+QjrgEASnISFnTtmACUoPbjodK9+MqPAVAzgQ0AAMD0iGvyEdcAAKVJcc1JxZ9/tb0Axvau8s+/9CMA1E5gAwAAMC3imnzENQBAiWo/HmrpRwAoRO27if3bjwBQO4ENAADAdIhr8hHXAAAlcjxUxF9+DIAC1L6bWHLhxwConcAGAABgGsQ1+YhrAIBSef6zgw1QhreVf/7L9XXlxwConcAGAACgfOKafMQ1AEDJ3lX++dOC7sqPATAyu4mJHQG+E9gAAACUTVyTj7gGACjZ6fpqKp/B0o8BUADv5I7rA/hOYAMAAFAucU0+4hoAoHTvjMCCLuB+XIilEQAIbAAAAEolrslHXAMAlM5xJBtLIwBGtgi7iaV78ZUfBQCBDQAAQInENfmIawCAKUjPgieVz2AZFnSB8b01gvjDCAA2BDYAAABlEdfkI64BAKbCcSQWdIHxNd7Pv1saAcCGwAYAAKAc4pp8xDUAwFQswnEkyYURACPzfh6xWl+XxgCw8Q8jAAAAGF3a/v9bbBZT6J+4BgCYko9G8H1Bd2UMwMjsJiZ2BLhFYAMAADCuFNf8ub5OjSILcQ0AMCVNiK4TC7rA2Nrt+3rt/jICgGuOiAIAABiPuCYvcQ0AMDV2r9n43QgA9+PRXYXgEeAWgQ0AAMA4xDV5iWsAgKlpYrNjQu3Sgu6lMQAjarf35NqJawDuENgAAAAMT1yTl7gGAJiid0bwnQVdYGxvjeA7x0MB3CGwAQAAGJa4Ji9xDQAw1WfE1hi++8MIgBEttheCR4B7BDYAAADDEdfkJa4BAKbq/fZZsXbpeCgLusCYPhrBdxfbezIANwhsAAAAhiGuyUtcAwBM+TnR8VAb4hpgTIuwe82O3cQAHiCwAQAAyE9ck5e4BgCYMrvXXLOgC4zJ7jUbdhMDeITABgAAIC9xTV7iGgBg6s+Kdq/ZsKALjGkRdq/ZcTwUwCMENgAAAPmIa/IS1wAAU/cp7F6z47kOGPt+zIbdxAAeIbABAADIQ1yTl7gGAJi6Zn21xvDD70YAjKT17v6D3cQAniCwAQAA6J+4Ji9xDQAwBx+N4IfV+ro0BsD9eHTetQGeILABAADol7gmL3ENADAHi7B7zU1fjAAYyVlsdhRjw25iAE8Q2AAAAPRHXJOXuAYAmAu7JdzmOBJgrHf4d8bwwyrsJgbwJIENAABAP8Q1eYlrAIC5aGOzgw0bKa5ZGQMwgo/bd3k27CYG8AyBDQAAwPHENXmJawCAOT032r3mtj+MABhBen9/bwy3eO8GeIbABgAA4DjimrzENQDAnKTF3MYYfrjyrAeM5JMR3NJt78kAPEFgAwAAcDhxTV7iGgBgTpqwe81dnvWAMbThqL67fjcCgOcJbAAAAA4jrslLXAMAzM1XI7jnixEAI7zL273mttX6WhoDwPMENgAAAC8nrslLXAMAzE0bdku4axmbRV2AIX3cvtNzTewIsCeBDQAAwMuIa/IS1wAAc3x+tFvCfRZ0gaEt1td7Y7jHOzjAngQ2AAAA+xPX5CWuAQDm6GvYLeGu1fq6MAZg4Pd5R/Xdl97Br4wBYD8CGwAAgP2Ia/IS1wAAc/R6e3Gb3WuAoaWdaxpjcD8GOIbABgAA4HnimrzENQDAXJ8h7ZZw35VnP2Bg6V3+ozHcs1xfl8YAsD+BDQAAwNPENXmJawCAuXI01MPS0VCOIwGGfKf/ZgwPsnsNwAsJbAAAAB4nrslLXAMAzJWjoR53bgTAgNLONY0x3LOKTfAIwAsIbAAAAB4mrslLXAMAzPk50tFQD0uLuStjAAayWF/vjeFBYkeAAwhsAAAA7hPX5CWuAQDm7Fs4GuoxjiMBhnyvdzTUw9IxfXavATiAwAYAAOA2cU1e4hoAYM7STgkLY3jQcnsBDOFriB0fk2LHK2MAeDmBDQAAwDVxTV7iGgBgztIz5CdjeJTjSIChpNjxtTE8KIU1n40B4DACGwAAgA1xTV7iGgBg7s+SjiJ53CrsXgMMQ+z4NLvXABxBYAMAACCuyU1cAwDMXTqKpDGGR9m9Bhjq3V7s+DTv5gBHENgAAAC1E9fkJa4BAObuLBxF8pSV50FgICmuaYzhUd32ngzAgQQ2AABAzcQ1eYlrAIC5W6yvj8bwJLvXAEM4296TcT8GyEZgAwAA1Epck5e4BgCYuyYcRfKclWdCYABpFzGx49O6sHsNwNEENgAAQI3ENXmJawCAGp4nv23/5HF2SwByS+/1X43B/RhgCAIbAACgNuKavMQ1AEANvnqefNbKcyEwwPu92PF552H3GoBeCGwAAICaiGvyEtcAADX4FJvjSHjaByMAMkvv940xPOlqfX02BoB+CGwAAIBaiGvyEtcAADVo19d7Y3jWcn1dGAOQkZ3E9vMlNpENAD0Q2AAAADUQ1+QlrgEAapB2rflqDHs5NwIgo7STWGsMz7J7DUDPBDYAAMDciWvyEtcAADVIz5Limv0stxdADm3YSWxfKXa0ew1AjwQ2AADAnIlr8hLXAAA1ON0+U54Yxd7PiAA5tCF23Ncq7F4D0DuBDQAAMFfimrzENQBALc+U30Jcs6/0fLgyBiADO4m9zAcjAOifwAYAAJgjcU1e4hoAoKZnysYo9pKOIbGgC+Sw20mM/SzX14UxAPRPYAMAAMyNuCYvcQ0A4JmSh3yJTWQD0CfH9L2c2BEgE4ENAAAwJxZC8hLXAACeKXnIan2dGQPQM3HNy6V39ktjAMhDYAMAAMyFhZC8xDUAgGdKHmO3BKBv4pqXc1QfQGYCGwAAYA4shOQlrgEAPFPymOX6ujAGoEfimsOch6P6ALIS2AAAAFNnISQvcQ0A4JmS554XAfoirjlMOhbqszEA5CWwAQAApsxCSF7iGgDAMyVPSbslrIwB6Im45nCOhgIYgMAGAACYKgsheYlrAADPlDxlFXZLAPojrjlcendfGgNAfgIbAABgiiyE5CWuAQBq0HimPPqZ8coYgB68DnHNodJ92O41AAP5hxEAAAATI67JS1wDANTATgnHuQi7JQD9aNfXV2M4WIprxI4AA7GDDQAAMCXimrzENQBADcQ1x7naPjcCHOt9iGuOsfQODzAsgQ0AADAV4pq8xDUAQA3a9fV3iGuOcR52SwCOl8KaT8ZwMLEjwAgENgAAwBSIa/IS1wAANTgLOyUca7m+PhsDcOT7/bfYBI8cLsWOK2MAGNY/jAAAACicuCYvcQ0AUMPzZNoloTWKo9gtAThWE5u4xvv9cS5D7AgwCoENAABQMnFNXuIaAMDzJPuyWwJwjNPt/dgRff28ywMwAkdEAQAApbIYkpe4BgCYu/Qc+R/Pk71Yht0SgMO16+vvENf04UNsdrABYAQCGwAAoETimrzENQDA3LVhMbcvjoYCjnm3/7q9ON4yxI4Ao3JEFAAAUBpxTV7iGgBg7s+Sn2IT2NCPtFvCyhiAF2rW1zfv9r0ROwIUwA42AABAScQ1eYlrAIA5O90+S7ZG0ZsLz4/AAV7HZhcx7/b9ETsCFEBgAwAAlEJck5e4BgCYs9azZO9WYbcE4OXSLmLfwhF9fRI7AhTCEVEAAEAJxDV5iWsAgDk/RzoSKt8z5JUxAHtK7/Nfvdf3ztFQAAWxgw0AADA2cU1e4hoAYK4WsTmCpDWK3p2vr6UxAHt6770+mzchdgQohh1sAACAMYlr8hLXAABzdba+PhpDFpfb+QLs806fjoNaGEUWYkeAwghsAACAsYhr8hLXAABz5AiSvNIuCW+MAdjD6+39+MQosliG2BGgOI6IAgAAxiCuyUtcAwDM0VlsjoTyDJn3OXJlDMAz7/Pftpe4Jg+xI0Ch7GADAAAMTVyTl7gGAJibRWx2SWiMIqvP6+vCGIAnvI/N8XzCmrxSXHNlDADlsYMNAAAwJHFNXuIaAGBuz46fts+PjXFktVxfH4wBeMTp9l78KcQ1uZ1v78kAFMgONgAAwFDENXmJawCAOWnDQu5QHEUCPPUev9u1hvzSLmJnxgBQLoENAAAwBHFNXuIaAGAu0vNiCmsWRjGYX8JRJMB9bWzCmsYoBrHavtsDUDCBDQAAkJu4Ji9xDQAwl2fGFNa0RjH4s+SlMQA3LGIT1iyMYjC7ncTEjgCFE9gAAAA5iWvyEtcAAHN4XkzHj7wLx0ENrfMsCdzQxCasaY1icB9C7AgwCQIbAAAgF3FNXuIaAGDq2tjsWiOsGd5lOIoEuH53T6HjR6MYxbl3e4DpENgAAAA5iGvyEtcAAFPWxmYhtzGKUazW1y/GAN7bww5iY7tYX2fGADAdAhsAAKBv4pq8xDUAwFS1IawZ29X6erP9E6j3nV1YMz47iQFMkMAGAADok7gmL3ENADBFbQhrSnqevDQGqPZ9XVhThhQ5/hJiR4DJEdgAAAB9EdfkJa4BAKb2bPg6hDWlPU9eGANUJ92DU1TThrCmBOIagAkT2AAAAH0Q1+QlrgEApqKJzSKuHRLK0nmehOqcxnVYQ1nv93YSA5gogQ0AAHAscU1e4hoAYAoW6+ttWMgtUbd9pgTq0G7vxwujKPL93k5iABMmsAEAAI4hrslLXAMAlP4smI6Beud5sFhpl4QPxgCz18R1WNMYR5E+e78HmD6BDQAAcChxTV7iGgCgVLtjR16HY6BKluKaX9bXlVHAbLXr69ft/ZhypXd7sSPADAhsAACAQ4hr8hLXAAClaeJ6t5rGOIqXohpxDcyTyHFaunBMH8BsCGwAAICXEtfkJa4BAEp67mtjc+SIZ7/pENfA/Jxu78UpqmmMYzIc0wcwMwIbAADgJcQ1eYlrAICxNbFZwE1HjiyMY3J2cc2lUcDkiWqmzTF9ADMksAEAAPYlrslLXAMAjCU93+2iGs960yWugem7GTg2xjFZ4hqAmRLYAAAA+xDX5CWuAQCG1MRm8fZVbBZzT4xkFt6EuAam5vTO/ZjpE9cAzJjABgAAeI64Ji9xDQCQWxPXC7iLsCvCXJ8pl8YAxTu9cz8WOM7LKsQ1ALMmsAEAAJ4irslLXAMA5LDYXj9tn+MaI/FMCQyu2d6D0/Vq+6egZr5SVPMmxDUAsyawAQAAnvI1xDW5WAgBAI7VxPXuNGIaz5TAeE5v3IPFNPVJUU3aucYxfQAzJ7ABAACe4i8E87AQAgDsa7dIu/szhTRNiKDxTAlDW9z4859xHdQ0RlM1cQ1ARQQ2AAAAw7IQAgB1auLhRdjFjf87xTMnD/zv4JkS+vPQ7jK7kHHn1SP/O9x0ub0fi2sAKiGwAYj4f0YAMBnL2PyrIJgqCyFAKf4Mi/cAU3S1faa8MIos2tgckwvwnBTV/LK9LwNQCYENAADAMMQ1AAAcwzEk+e2e10U2wFPENQCV+h8jAAAAyE5cAwDAMcQ1w+m2z+8ADxHXAFRMYAMAAJCXuAYAgGOIa4bXhcgGuE9cA1A5gQ0AAEA+4hoAAI6RFnN/DnHNGLoQ2QC37wnpfiyuAajYP4wAAAAgC3ENAADHsFPC+HbP81+NAqq/FwjuALCDDQAAQAbiGgAAjnER4ppSdGFhHWr2wT0AgB072AAAAPRLXAMAwDG6sJj7/9m71+O4jSwAo7ccwWaw5RAcgkJQBq0QmIGUAZXBVQbcDMYZjDOAM+BmsIOdhkVJpDQP9AzQOKcK1Sz/bJKoofvT7SV+T0Ym2YC/7wHYMIENAADAfPzPNwAArjFOSni0DYs0fc4X2UD/xulh4xSxva0A4CVXRAEAAMxDXAMAwKWe6+dJcc2yZZguBL0bQlwDwBtMsAEAALieuAYAgEuZlLAu0+d+k2ygP/v6Pn62FQC8xgQbAACA64hrAAC41HiY+3uIa9YmwyQb6PH3+o8Q1wDwEwIbAACAy4lrAAC41Pg50qSEdX//RDbQhwe/zwCcwhVRAAAAlxHXAABwqfEw99E2rN7094DromCdxsDx/eHZ2QoATiGwAQAAOJ+4BgCASzjM7c/0d4HIBtZlX9/Hg60A4FSuiAIAADiPuAYAgEuMh7l/hLimRxmul4G1/c6OV/QNtgKAcwhsAAAATieuAQDgEuNnSIe5/X+PRTawbM/19/RD/RoAzuKKKAAAgNOIawAAONd4gPvgc+RmTN9n10XB8gxxvBJqbysAuJQJNgAAAL8mrgEA4FzjIe47nyM3J8MkG1iapzhe0SeuAeAqAhsAAICfE9cAAHCu8fPjGNc4zN3u919kA/c3XQn1PlwJBcAMXBEFAADwNnENAADnmA5zn2zF5k1/R7guCu5jX9/HQkcAZmOCDQAAwOvENQAAnGMXxytIxDVMMkyygXt4DFdCAdCACTYAAAA/EtcAAHCOhzge6ML3pr8rTLKB9ob69/zOVgDQgsAGAADgW7sQ1wAAcBpXkHCK6e8LkQ2081Tfx8+2AoBWXBEFAAAAAADn+xSuIOF0Ga6LghbGoOZ9fcQ1ADRlgg0AAAAAAJzO1BoulXU1yQbmYWoNADdlgg0AAAAAAJzG1BqulWGSDVzL1BoA7sIEGwAAAAAA+LldHKOIwVYwg6yrSTZwvsc4xo7CGgBuzgQbAAAAAAB43XiA+3B43oW4hnllmGQD59jXd/FDiGsAuBMTbAAAAAAA4EcZDnJp/zM2MskG3ja+gz8fno+2AoB7E9gAAAAAAMBX45SEMazZ2QpuIOsqsoEfPdX38WArAFgCgQ0AAAAAABynJHw6PI+2ghvLuops4GiI4xVqO1sBwJL8ZgsAAAAAANi4PDy/h7iG+/4MfrANbNwYOj7U9/HOdgCwNCbYAAAAAACwVbs4HububQULkHU1yYYtGgPHcYrYs60AYKkENgAAAAAAbM0Qrh9hmbKuIhu2Ylffx4OtAGDpXBEFAAAAAMBWDHE8yHX9CEuW4boo+je+g9/VZ7AdAKyBCTYAAAAAAPRuvHLk8+H5aCtYiayrSTb0Zojj1XxPtgKAtRHYAAAAAADQqymseaxfw5pkXUU29GA4PJ9e/FwDwOoIbAAAAAAA6I2whl5kXUU2rNUQwhoAOiGwAQAAAACgF8IaepR1FdmwJkMIawDojMAGAAAAAIC1Gw7PlxDW0K+sq8iGpdvHMXRMWwFAbwQ2AAAAAACs1RAmJLAd08+5yIYl2tX38c5WANArgQ0AAAAAAGuzi+OEhCdbwcZkXUU2LOlncnwf720FAL0T2AAAAAAAsBYZDnIh6yqy4V6e4+s1UIPtAGArBDYAAAAAACzZcHi+HJ7HOB7qAiIb7mOMGz+Ha/kA2CiBDQAAAAAASzRe//QlXAMFb8m6imy4xc+a6WEAbJ7ABgAAAACApRjiGNVkuHYETpF1Fdkwt2lazRg5mh4GACGwAQAAAADg/vLw/CdMq4FLf39GIhuu9Vzfw6bVAMArBDYAAAAAANyD6Qgwn6yryIZLuJIPAE4gsAEAAAAA4FbGqGY6xB1sB8wq6yqy4RTje3iaHCZyBIATCGwAAAAAAGhJVAO3k3UV2fAaUQ0AXEFgAwAAAADA3EQ1cD9ZV5ENEaIaAJiNwAYAAAAAgGuNh7a7cIgLS5F1Fdlsz/Dd+xgAmInABgAAAACAS4xTanZxPMTd2Q5YnKyryKZ/L9/Fe9sBAG0IbAAAAAAAOMUQ3x7imlIDy5d1Fdn0ZQoc/wxTagDgZgQ2AAAAAAC85uUB7vj1YEtglbKuIpv12tX38J8hcASAuxHYAAAAAAAwHta+PLzdhwNc6EnWVWSzfEN9B/9V38c7WwIAyyCwAQAAAADYlqE+02Qa02lgG7KuIptlvY9fxjTiRgBYMIENAAAAAECfpqk04/P3i68d3sJ2ZV1FNrc1xNeYZnof72wLAKyLwAYAAAAAYL2GF890aDsGNDtbA7wh6yqyaWN8B3/2PgaA/ghsAAAAAACWZ/fi6/GQ9r/xdSLN9N9MogEulXUV2czvX4fn34fno60AgL4IbAAiPtkCgNUYbAEAcMXniC+2gQX8HL71mXZne4Aby7qKbOZX6vrBVgBAPwQ2AP4lAQAAwBYM/v4DgB9kXUU28yt1FdkAQCd+swUAAAAAAACblSECaaWEeAkAuiGwAQAAAAAA2LYMkU0rJUQ2ANAFgQ0AAAAAAAAZIptWSohsAGD1BDYAAAAAAACMMkQ2rZQQ2QDAqglsAAAAAAAAmGSIbFopIbIBgNUS2AAAAAAAAPBShsimlRIiGwBYJYENAAAAAAAA38sQ2bRSQmQDAKsjsAEAAAAAAOA1GSKbVkqIbABgVQQ2AAAAAAAAvCVDZNNKCZENAKyGwAYAAAAAAICfyRDZtFJCZAMAqyCwAQAAAAAA4FcyRDatlBDZAMDiCWwAAAAAAAA4RYbIppUSIhsAWDSBDQAAAAAAAKfKENm0UkJkAwCLJbABAAAAAADgHBkim1ZKiGwAYJEENgAAAAAAAJwrQ2TTSgmRDQAsjsAGAAAAAACAS2SIbFopIbIBgEUR2AAAAAAAAHCpDJFNKyVENgCwGAIbAAAAAAAArpEhsmmlhMgGABZBYAMAAAAAAMC1MkQ2rZQQ2QDA3QlsAAAAAAAAmEOGyKaVEiIbALgrgQ0AAAAAAABzyRDZtFJCZAMAdyOwAQAAAAAAYE4ZIptWSohsAOAuBDYAAAAAAADMLUNk00oJkQ0A3JzABgAAAAAAgBYyRDatlBDZAMBNCWwAAAAAAABoJUNk00oJkQ0A3IzABgAAAAAAgJYyRDatlBDZAMBNCGwAAAAAAABoLUNk00oJkQ0ANCewAQAAAAAA4BYyRDatlBDZAEBTAhsAAAAAAABuJUNk00oJkQ0ANCOwAQAAAAAA4JYyRDatlBDZAEATAhsAAAAAAABuLUNk00oJkQ0AzE5gAwAAAAAAwD1kiGxaKSGyAYBZCWwAAAAAAAC4lwyRTSslRDYAMBuBDQAAAAAAAPeUIbJppYTIBgBmIbABAAAAAADg3jJENq2UENkAwNUENgAAAAAAACxBhsimlRIiGwC4isAGAAAAAACApcgQ2bRSQmQDABcT2AAAAAAAALAkGSKbVkqIbADgIgIbAAAAAAAAliZDZNNKCZENAJxNYAMAAAAAAMASZYhsWikhsgGAswhsAAAAAAAAWKoMkU0rJUQ2AHAygQ0AAAAAAABLliGyaaWEyAYATiKwAQAAAAAAYOkyRDatlBDZAMAvCWwAAAAAAABYgwyRTSslRDYA8FMCGwAAAAAAANYiQ2TTSgmRDQC8SWADAAAAAADAmmSIbFopIbIBgFcJbAAAAAAAAFibDJFNKyVENgDwA4ENAAAAAAAAa5QhsmmlhMgGAL4hsAEAAAAAAGCtMkQ2rZQQ2QDAPwQ2AAAAAAAArFmGyKaVEiIbAPg/gQ0AAAAAAABrlyGyaaWEyAYABDYAAAAAAAB0IUNk00oJkQ0AGyewAQAAAAAAoBcZIptWSohsANgwgQ0AAAAAAAA9yRDZtFJCZAPARglsAAAAAAAA6E2GyKaVEiIbADZIYAMAAAAAAECPMkQ2rZQQ2QCwMQIbAAAAAAAAepUhsmmlhMgGgA0R2AAAAAAAANCzDJFNKyVENgBshMAGAAAAAACA3mWIbFopIbIBYAMENgAAAAAAAGxBhsimlRIiGwA6J7ABAAAAAABgKzJENq2UENkA0DGBDQAAAAAAAFuSIbJppYTIBoBOCWwAAAAAAADYmgyRTSslRDYAdEhgAwAAAAAAwBZliGxaKSGyAaAzAhsAAAAAAAC2KkNk00oJkQ0AHRHYAAAAAAAAsGUZIptWSohsAOjE/wRg796O5DiSNIz+D6vIarAUASJQAx8RqMGMBDsiuAgUgaNBiwARsBosCmiQANEI9KW8Ki/nmIXVu2c9uFl+liGwAQAAAAAA4Ow6IpspFZENAAcgsAEAAAAAAACRzaSKyAaAnRPYAAAAAAAAwGcdkc2UisgGgB0T2AAAAAAAAMBfOiKbKRWRDQA7JbABAAAAAACAb3VENlMqIhsAdkhgAwAAAAAAAN/riGymVEQ2AOyMwAYAAAAAAACe1hHZTKmIbADYEYENAAAAAAAA/FhHZDOlIrIBYCcENgAAAAAAALDWEdlMqYhsANgBgQ0AAAAAAAD8XEdkM6UisgFg4wQ2AAAAAAAA8Dwdkc2UisgGgA0T2AAAAAAAAMDzdUQ2UyoiGwA2SmADAAAAAAAAL9MR2UypiGwA2CCBDQAAAAAAALxcR2QzpSKyAWBjBDYAAAAAAADwOh2RzZSKyAaADRHYAAAAAAAAwOt1RDZTKiIbADZCYAMAAAAAAABv0xHZTKmIbADYAIENAAAAAAAAvF1HZDOlIrIB4M4ENgAAAAAAAHAdHZHNlIrIBoA7EtgAAAAAAADA9XRENlMqIhsA7kRgAwAAAAAAANfVEdlMqYhsALgDgQ0AAAAAAABcX0dkM6UisgHgxgQ2AAAAAAAAMKMjsplSEdkAcEMCGwAAAAAAAJjTEdlMqYhsALgRgQ0AAAAAAADM6ohsplRENgDcgMAGAAAAAAAA5nVENlMqIhsAhglsAAAAAAAA4DY6IpspFZENAIMENgAAAAAAAHA7HZHNlIrIBoAhAhsAAAAAAAC4rY7IZkpFZAPAAIENAAAAAAAA3F5HZDOlIrIB4MoENgAAAAAAAHAfHZHNlIrIBoArEtgAAAAAAADA/XRENlMqIhsArkRgAwAAAAAAAPfVEdlMqYhsALgCgQ0AAAAAAADcX0dkM6UisgHgjQQ2AAAAAAAAsA0dkc2UisgGgDcQ2AAAAAAAAMB2dEQ2UyoiGwBeSWADAAAAAAAA29IR2UypiGwAeAWBDQAAAAAAAGxPR2QzpSKyAeCFBDYAAAAAAACwTR2RzZSKyAaAFxDYAAAAAAAAwHZ1RDZTKiIbAJ5JYAMAAAAAAADb1hHZTKmIbAB4BoENAAAAAAAAbF9HZDOlIrIB4CcENgAAAAAAALAPHZHNlIrIBoAFgQ0AAAAAAADsR0dkM6UisgHgBwQ2AAAAAAAAsC8dkc2UisgGgCcIbAAAAAAAAGB/OiKbKRWRDQB/I7ABAAAAAACAfeqIbKZURDYAfEVgAwAAAAAAAPvVEdlMqYhsAHgksAEAAAAAAIB964hsplRENgBEYAMAAAAAAABH0BHZTKmIbABOT2ADAAAAAAAAx9AR2UypiGwATk1gAwAAAAAAAMfREdlMqYhsAE5LYAMAAAAAAADH0hHZTKmIbABOSWADAAAAAAAAx9MR2UypiGwATkdgAwAAAAAAAMfUEdlMqYhsAE5FYAMAAAAAAADH1RHZTKmIbABOQ2ADAAAAAAAAx9YR2UypiGwATkFgAwAAAAAAAMfXEdlMqYhsAA5PYAMAAAAAAADn0BHZTKmIbAAOTWADAAAAAAAA59ER2UypiGwADktgAwAAAAAAAOfSEdlMqYhsAA5JYAMAAAAAAADn0xHZTKmIbAAOR2ADAAAAAAAA59QR2UypiGwADkVgAwAAAAAAAOfVEdlMqYhsAA5DYAMAAAAAAADn1hHZTKmIbAAOQWADAAAAAAAAdEQ2UyoiG4DdE9gAAAAAAAAAFx2RzZSKyAZg1wQ2AAAAAAAAwBcdkc2UisgGYLcENgAAAAAAAMDXOiKbKRWRDcAuCWwAAAAAAACAv+uIbKZURDYAuyOwAQAAAAAAAJ7SEdlMqYhsAHZFYAMAAAAAAAD8SEdkM6UisgHYDYENAAAAAAAAsNIR2UypiGwAdkFgAwAAAAAAAPxMR2QzpSKyAdg8gQ0AAAAAAADwHB2RzZSKyAZg0wQ2AAAAAAAAwHN1RDZTKiIbgM0S2AAAAAAAAAAv0RHZTKmIbAA2SWADAAAAAAAAvFRHZDOlIrIB2ByBDQAAAAAAAPAaHZHNlIrIBmBTBDYAAAAAAADAa3VENlMqIhuAzRDYAAAAAAAAAG/REdlMqYhsADZBYAMAAAAAAAC8VUdkM6UisgG4O4ENAAAAAAAAcA0dkc2UisgG4K4ENgAAAAAAAMC1dEQ2UyoiG4C7EdgAAAAAAAAA19QR2UypiGwA7kJgAwAAAAAAAFxbR2QzpSKyAbg5gQ0AAAAAAAAwoSOymVIR2QDclMAGAAAAAAAAmNIR2UypiGwAbkZgAwAAAAAAAEzqiGymVEQ2ADchsAEAAAAAAACmdUQ2UyoiG4BxAhsAAAAAAADgFjoimykVkQ3AKIENAAAAAAAAcCsdkc2UisgGYIzABgAAAAAAALiljshmSkVkAzBCYAMAAAAAAADcWkdkM6UisgG4OoENAAAAAAAAcA8dkc2UisgG4KoENgAAAAAAAMC9dEQ2UyoiG4CrEdgAAAAAAAAA99QR2UypiGwArkJgAwAAAAAAANxbR2QzpSKyAXgzgQ0AAAAAAACwBR2RzZSKyAbgTQQ2AAAAAAAAwFZ0RDZTKiIbgFcT2AAAAAAAAABb0hHZTKmIbABeRWADAAAAAAAAbE1HZDOlIrIBeDGBDQAAAAAAALBFHZHNlIrIBuBFBDYAAAAAAADAVnVENlMqIhuAZxPYAAAAAAAAAFvWEdlMqYhsAJ5FYAMAAAAAAABsXUdkM6UisgH4KYENAAAAAAAAsAcdkc2UisgGYElgAwAAAAAAAOxFR2QzpSKyAfghgQ0AAAAAAACwJx2RzZSKyAbgSQIbAAAAAAAAYG86IpspFZENwHcENgAAAAAAAMAedUQ2UyoiG4BvCGwAAAAAAACAveqIbKZURDYAfxLYAAAAAAAAAHvWEdlMqYhsAD4R2AAAAAAAAAB71xHZTKmIbAAENgAAAAAAAMAhdEQ2UyoiG+DkBDYAAAAAAADAUXRENlMqIhvgxAQ2AAAAAAAAwJF0RDZTKiIb4KQENgAAAAAAAMDRdEQ2UyoiG+CEBDYAAAAAAADAEXVENlMqIhvgZAQ2AAAAAAAAwFF1RDZTKiIb4EQENgAAAAAAAMCRdUQ2UyoiG+AkBDYAAAAAAADA0XVENlMqIhvgBAQ2AAAAAAAAwBl0RDZTKiIb4OAENgAAAAAAAMBZdEQ2UyoiG+DABDYAAAAAAADAmXRENlMqIhvgoAQ2AAAAAAAAwNl0RDZTKiIb4IAENgAAAAAAAMAZdUQ2UyoiG+BgBDYAAAAAAADAWXVENlMqIhvgQAQ2AAAAAAAAwJl1RDZTKiIb4CAENgAAAAAAAMDZdUQ2UyoiG+AABDYAAAAAAAAAIptJFZENsHMCGwAAAAAAAIDPOiKbKRWRDbBjAhsAAAAAAACAv3RENlMqIhtgpwQ2AAAAAAAAAN/qiGymVEQ2wA4JbAAAAAAAAAC+1xHZTKmIbICdEdgAAAAAAAAAPK0jsplSEdkAOyKwAQAAAAAAAPixjshmSkVkA+yEwAYAAAAAAABgrSOymVIR2QA7ILABAAAAAAAA+LmOyGZKRWQDbJzABvjaPx+XQwAAAAAAAL7XEdlMqYhsgA0T2AB/d1kK/20MAAAAAAAAT+qIbKZURDbARglsgKf8ZjEEAAAAAAD4oY53KVMqIhtggwQ2wGox/PXj+WAUAAAAAAAA3+mIbKZURDbAxghsgJXfP553EdkAAAAAAAA8pSOymVIR2QAbIrABfubh4/nvx18AAAAAAAC+1RHZTKmIbICNENgAz3H5gs3lSzZ/GAUAAAAAAMB3OiKbKRWRDbABAhvgub5ENm0UAAAAAAAA3+mIbKZURDbAnQlsgJe6LIb/MgYAAAAAAIDvdEQ2UyoiG+COBDbAa/zTcggAAAAAAPCkjvcoUyoiG+BOBDbAW5bDy5VRH4wCAAAAAADgGx2RzZSKyAa4A4EN8BZ/RGQDAAAAAADwlI7IZkpFZAPcmMAGeKuHj+e/H38BAAAAAAD4S0dkM6UisgFuSGADXMPlCzaXL9n8YRQAAAAAAADf6IhsplRENsCNCGyAa/kS2bRRAAAAAAAAfKMjsplSEdkANyCwAa7tshz+yxgAAAAAAAC+0RHZTKmIbIBhAhtgwj8tiAAAAAAAAN/peIcypSKyAQYJbIDJBfFyZdQHowAAAAAAAPhTR2QzpSKyAYYIbIBJf+RzZPPeKAAAAAAAAP7UEdlMqYhsgAECG2Daw8fzy+MvAAAAAAAAn3VENlMqIhvgygQ2wC1crom6fMnmd6MAAAAAAAD4U0dkM6UisgGuSGAD3Molsvn1cVEEAAAAAADgs47IZkpFZANcicAGuLXLgvibMQAAAAAAAPypI7KZUhHZAFcgsAHu4d+WRAAAAAAAgG90vD+ZUhHZAG8ksAHuuST+ks9XRwEAAAAAACCymVQR2QBvILAB7unh43n38bw3CgAAAAAAgE86IpspFZEN8EoCG+DeLpHNL4+/AAAAAAAAiGwmVUQ2wCsIbIAtuFwTdfmSze9GAQAAAAAA8ElHZDOlIrIBXkhgA2zFJbL59XFZBAAAAAAAQGQzqSKyAV5AYANszWVJ/M0YAAAAAAAAPumIbKZURDbAMwlsgC369+Oi+MEoAAAAAAAARDaDKiIb4BkENsCWF8V3EdkAAAAAAABcdEQ2UyoiG+AnBDbAlj3kc2TzYBQAAAAAAAAim0EVkQ2wILABtk5kAwAAAAAA8JeOyGZKRWQD/IDABtiDyzVR7x4XRgAAAAAAgLPriGymVEQ2wBMENsBefHhcFNsoAAAAAAAARDaDKiIb4G8ENsDe/MOyCAAAAAAA8EnHe5MpFZEN8BWBDbDnZfGDUQAAAAAAACfXEdlMqY/nf40BuBDYAHteFt9FZAMAAAAAANAR2Uz5HyMALgQ2wJ495HNk82AUAAAAAADAyXVENgBjBDbA3olsAAAAAAAAPuuIbABGCGyAI7hcE/XucWkEAAAAAAA4s47IBuDqBDbAUXx4XBbbKAAAAAAAgJPriGwArkpgAxzNPyyMAAAAAAAAIhuAaxLYAEddGH/N56/aAAAAAAAAnFVHZANwFQIb4Kh+/3jeRWQDAAAAAACcW0dk8xb/MQLgQmADHNnDx/PL4y8AAAAAAMBZdUQ2AG8isAGO7n0+f8lGZAMAAAAAAJxZR2QD8GoCG+AMLtdE/fK4OAIAAAAAAJxVR2QD8CoCG+BMLgvjv40BAAAAAAA4sY7IBuDFBDbA2fxmaQQAAAAAAE6u430JwIsIbICzLo2/5vPVUQAAAAAAAGfUEdkAPJvABjir3z+edxHZAAAAAAAA59UR2QA8i8AGOLOHj+cXYwAAAAAAAE6sI7JZeTAC4EJgA5zdeyMAAAAAAABOriOy+RG3IQCfCGwAAAAAAAAA6IhsAH5IYAMAAAAAAADARUdkA/AkgQ0AAAAAAAAAX3RENgDfEdgAAAAAAAAA8LWOyAbgGwIbAAAAAAAAAP6uI7IB+JPABgAAAAAAAICndEQ2AJ8IbAAAAAAAAAD4kc65I5v3/gLAhcAGAAAAAAAAgJXOeSOb9x4/cCGwAQAAAAAAAOBnOq6LAk5MYAMAAAAAAADAc3RENsBJCWwAAAAAAAAAeK6OyAY4IYENAAAAAAAAAC/REdkAJyOwAQAAAAAAAOClOiIb4EQENgAAAAAAAAC8RkdkA5yEwAYAAAAAAACA1+ocN7L54PECXwhsAAAAAAAAAHiLzjEjmwePFvhCYAMAAAAAAADAW3VcFwUcmMAGAAAAAAAAgGvoiGyAgxLYAAAAAAAAAHAtHZENcEACGwAAAAAAAACuqSOyAQ5GYAMAAAAAAADAtXVENsCBCGwAAAAAAAAAmNAR2QAHIbABAAAAAAAAYEpHZAMcgMAGAAAAAAAAgEmdfUY2//HogC8ENgAAAAAAAABM6/iSDbBjAhsAAAAAAAAAbqEjsgF2SmADAAAAAAAAwK10RDbADglsAAAAAAAAALiljsgG2BmBDQAAAAAAAAC31hHZADsisAEAAAAAAADgHjoiG2AnBDYAAAAAAAAA3EtHZAPsgMAGAAAAAAAAgHvqbDOyefBogC8ENgAAAAAAAADcW2d7kc0HjwX4QmADAAAAAAAAwBZ0XBcFbJTABgAAAAAAAICt6IhsgA0S2AAAAAAAAACwJR2RDbAxAhsAAAAAAAAAtqYjsgE2RGADAAAAAAAAwBZ1RDbARghsAAAAAAAAANiqjsgG2ACBDQAAAAAAAABb1rlPZPPB6IEvBDYAAAAAAAAAbF3n9pHNg7EDXwhsAAAAAAAAANiDjuuigDsR2AAAAAAAAACwFx2RDXAHAhsAAAAAAAAA9qQjsgFuTGADAAAAAAAAwN50RDbADQlsAAAAAAAAANijjsgGuBGBDQAAAAAAAAB71RHZADcgsAEAAAAAAABgzzoiG2CYwAYAAAAAAACAvetcN7L5w0iBrwlsAAAAAAAAADiCji/ZAEMENgAAAAAAAAAcRUdkAwwQ2AAAAAAAAABwJB2RDXBlAhsAAAAAAAAAjqYjsgGuSGADAAAAAAAAwBF1RDbAlQhsAAAAAAAAADiqjsgGuAKBDQAAAAAAAABH1hHZAG8ksAEAAAAAAADg6Dovi2wejAz4msAGAAAAAAAAgDPoPD+y+T/jAr4msAEAAAAAAADgLDquiwJeQWADAAAAAAAAwJl0RDbACwlsAAAAAAAAADibjsgGeAGBDQAAAAAAAABn1BHZAM8ksAEAAAAAAADgrDoiG+AZBDYAAAAAAAAAnFlHZAP8hMAGAAAAAAAAgLPrfBvZvDcS4GsCGwAAAAAAAAD4NrJ5bxzA1/7LCAAAAAAAAADgkzYC4CkCGwAAAAAAAAD4SxsB8HeuiAIAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAD/z94d1caNhmEY9UWJFEIgBEIgDIQwaBkEgpdBIHQZDIRACIT9LY+0lXb1tE1mkrHnHOmT79/rR/4BAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAA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AAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgC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NgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAARtX8nE+AUck4AAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACNgAAAjYCAYAAAAADILPAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABANpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjVEMjA4OTI0OTNCRkRCMTE5MTRBODU5MEQzMTUwOEM4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ4RThERjcwNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ4RThERjZGNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTBhMjJkMGUtMjUzNy00ZjU1LWEzNTctZjE3Yzk0Y2ZlNTkxIiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OTg5YTAzY2YtNjlhZS0xZDQwLWI0OWYtOWQxMTFlMGU2YjM1Ii8+IDxkYzp0aXRsZT4gPHJkZjpBbHQ+IDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCI+UHJpbnQ8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl2Dyx0AAJydSURBVHja7N3/bVxVGoDhE0QBKWEaQEoJLiEdrDvYNICSiAIQFZBUsNkKGCrAiAIYKiBbgXcOMxP/UPISknjGnnke6Uj2hD/gs3WUe+/LuY8uLy8HAAAAAAAAAADwfl8ZAQAAAAAAAAAAfJjABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAMLXRgDsfPPk21P8z36yXhd++gAAACdtXhs+Ngb2aGkE8LcW2wV3ZbVdAH/rt4vvDAEQ2AAnad44/dd6Pd1epD8yEgAAgJM2rw+fGwMH9nbc/J+AVuv1x7WvV9uvL7b/LBy7s/X60Rg4gNv77Pz+f+/5s6VRAcBpeXR5eWkKwF+O/ASb21HNjb3QTx8AAOCkzWvGX4yBB2a1XfNB76/jKtDZfQ7H4PfhFBvuv110M/feP659L4iEI+IEG2Bygg1wzCqqAQAAgJ1dlODakYdkce139ukHfq/ng92fx1V0szQ2HpiXwyk23H9P/ubPl+NmfLMaN08sAwAeCIENcIwXM6IaAAAA/qk36/XMGDgiuwe+Z7c+X43Ng9156s1yOPGG++3V2LzCb2EUPGBnH/h8twf/ut2Xl0YFAPebwAY4BqIaAAAAPtc85UNgwylYbNe8j/J8+9k86WY5rqIbrzXhPnl97XcVjsnZez67GFcRpOgGAO6ZR5eXl6YA/OWbJ98+pH/dLxnVPPLTBwAAYO3P9XpsDPCX3UPeGZ8th1NuOJy5L/9uf+aELbd78S64EUDCAfx28Z0hAE6wAR4UJ9UAAABwl5bba05gcx9mrvPt96tx9ZB3OQQ37M+MCX4YTrHhdJ2Nm6fdXNzajwU3ALAnTrAB3rmnJ9jsI6pxgg0AAADT+Xr9aAzwUVbr9WZ4wMt+OMUGPmy53YvnnnxhHHA3nGADTAIb4J17FNgs1uvfY38n1QhsAAAAmOaD2z+NAT7Jcr3+OzYPeFfGwR2YAeS5MUB6O67ixzdD/AhfjMAGmAQ2wDsHDmwWYxPUzNNqnux7L/TTBwAAYOuXA1yXwrFZjc2D3dfDaQp8OYuxOcUG+HgX271Y/AifSWADTF8ZAXDgi+JnY3Pzcl4cfz/cxAQAAOCwXhsBfLbFuHnPZ5488tRY+Eyr9XplDPCPzPvt32/34rknvxjuwQPAJxPYAPu2GKIaAAAA7q+lEcAXtRib1/r8Z2xewTZjmzNj4RO9NAL4ZPM+/PNxdW/+2XaPBgA+ksAG2IfFENUAAADwMMxXKayMAe7E47GJbX4a7hHxaeb+/MYY4LMtxs2TbZ5t92gAIAhsgLv8C7qoBgAAgIfIw1u4e4txde/Iw13+iR+MAL6o3Wuk5ilj87SxcyMBgPcT2ABf0mKIagAAAHj4fjYC2KvbD3efGglhObzOD+7K3H9/3O7H7u8DwC0CG+BzLYaoBgAAgOMyT7B5awxwEPPh7oxs5n2mF2Nz7wlue2kEcKfmiWLXTxk7H04ZAwCBDfBJFkNUAwAAwHHzmig4rMV6PR+be0/zNIUzI+Ga5XpdGAPsxZPtPrx7FrAwEgBOlcAG+FjzL82iGgAAgP1fi3EYXhMF98f5ev00Nvek5tdOUWD6wQhgr3an2sy9eJ40dmYkAJwagQ1QFkNUAwAAcCjn22sxD5IPwwk2cP8sxtUpCi+GCPHUvVqvlTHAQczX+V0PHwHgJAhsgNsWQ1QDAABwaOdj8xB5emocB/F2bF5BAtw/Mzy8/vqohZGcrJdGAAe12O7Df45N+CgMB+CoCWyA63bHO4pq4P/s3e9120i64OF375nvo41gcCNodQRmR9B2BEZHYDsCSxHYjsDoCKyOoNkRtCaC4WSgDHZZJmn9lygSBRRQz3MOju/OfuJLNQS4fq4CAIDxtHEd1ySvjGQ0fxgBTOKe6biSenVhFxsowc3wMa0vNEYCwBwJbIC7D8EAAACMp43bcU1iB5vxOCYKpmN3XEm6FsZRld+NAIqR1hh2/5DXDmMAzI7ABgAAAKAMbdyPa5K0ULEwnlGsws4IMDXpfim0qcvn2BzrB5T3bCu0AWBWBDYAAAAA42vj4bhm51cjGo1dbGCaFiG0qUWKa74YAxT9nCu0AWAWBDYAAAAA42rj6bgmcUzUeP4wApi0RQhtamAXG5jGM6/QBoBJE9gAAAAAjKeN5+OapAkLEWNZhkVbmINFbCKbb+6ns5Tu03Ycg+k8//69vs5icxQqAEyGwAYAAABgHG3sF9fs2MVmPBZtYT7SvXS3g4KF3Xk5NwKYjHT//bi9H58ZBwBTIbABAAAAGF4bL4trkrfGNhrHRME878O7hV2hzTys1ldnDDApN0Ob1jgAKJ3ABgAAAGBYbbw8rklOwyLwWJZGALO0W9j9OyzszoVdbGCamu3zcTrKb2EcAJRKYAMAAAAwnDYOi2t2HBM1jqtwTBTMWRPXC7unxjFpK/drmLTF9l7sGD8AiiSwAQAAABhGG8fFNcmvxjiav4wAZm8Rm91sLOxO2xcjgFk8N6djo94bBQAlEdgAAAAA5NfG8XFNsjDK0dgRAeq6Z/8nHBs1VctwtB/MQQodP8UmfPQMDEARBDYAAAAAebXRT1yTpIUGx0SNY7W+Lo0BqnESjo2asnMjgNk4DcdGAVAIgQ0AAABAPm30F9fsvDLW0SyNAKqziM3uCWdhYXdq92tRJMzvuTrtLiY2B2A0AhsAAACAPNroP65JLCqM53cjgGp9DMeUTM0XI4DZSaHjt9jsaNMYBwBDE9gAAAAA9K+NPHFN0oTjSsaSdkO4MgaoVrr/pkXdT2E3mynoYnO8HzA/i9hEj++NAoAhCWwAAAAA+tVGvrhmZ2HMo7kwAqheWtC1m800nBsBzFYKHVPwaDcbAAYjsAEAAADoTxv545rkrVGP5g8jAMJuNlPRhV1sYO4WYTcbAAYisAEAAADoRxvDxDVJOiLKgu44lkYA3LDbzcbRfeX63Qhg9m7uZuMZGYBsBDYAAAAAx2tjuLhm57Wxj+IqHBMF3NbEJrI5M4oifd7eu4H5W6yv/3hOBiAXgQ0AAADAcdoYPq5JfjX60TgmCnjIx9iENo1RFCXFNV+MAaqRdrD5Fo7wAyADgQ0AAADA4doYJ65J/Mvc8SyNAHhEOirq7+3vB8phFxuoTzrC789whB8APRLYAAAAABymjfHimh2RzThW6+vSGIBHnGx/P3wNuyeUwvF+UKdd9PjeKADog8AGAAAA4OXaGD+uSRwTNR7HRAH7/K6we0I5zo0AqpWOi0rHRokeATiKwAYAAADgZdooI65JFr6O0dgJAdhHimv+DEdGlWC1vjpjgGqlnR//DtEjAEcQ2AAAAADsr41y4pqkCYsEY0lHRK2MAdjDzSOjGJddbKBu6dlZ9AjAwQQ2AAAAAPtpo8zF0de+mtEsjQB44e+RtHuCI0rGswo7kEHtRI8AHExgAwAAAPC8Nsr9S/hffT2j+cMIgBdKu479JxzxN6YvRgCE6BGAAwhsAAAAAJ7WRtn/wjUt1ja+plHYBQE4RFrMdUTJeJZhBzLg+jn6P+HIVQD2JLABAAAAeFwb09g+fuGrGo3IBjiUI0rGc24EwFaKHv8O0SMAexDYAAAAADysjeksfDomajyOiQKO/V3jiJLhLdfXpTEAN6Tn/k/GAMBTBDYAAAAA97UxrV0FXofF2bEsjQA4Ujqa5M9wRMnQvhgBcMf79fXNczUAjxHYAAAAANzWxjSP7Fj46kaxCrsgAMcT2Qyv297DAW56vb0fi2wAuEdgAwAAAHCtjWnGNYljosbzuxEAPUiLuX9vfxcxjHMjAB6QYsf/hOgRgDsENgAAAAAbbUw3rkle+wpHszQCoEfpd9F7YxhEF3axAR6Wokc7iwFwi8AGAAAAYPpxTZIWASwAjCMdEbUyBqBHn2bwe2kq7EIGPPV8bWcxAH4Q2AAAAAC1a2M+i5hvfZ2juTACwO+nSfq8vq6MAXjC1xDZABACGwAAAKBubcxr8XLhKx3NX0YAZPo9lXZPODGKbFJc88UYgGekd4ZPxgBQN4ENAAAAUKs25rczQDoiqvHVjiLtYGMHBCDXvf3PENnkZBcbYB/vw85iAFUT2AAAAAA1amO+fzn+2tc7mqURAJmIbPJKcY2j/oDa3yMAeIbABgAAAKhNG/P+S/FXvuLR/GEEQEa7yKYxiizOjQDwPgHAU/5hBAAAAEBF2pj/X4anHWzSDgeOuhhe2v1gZQw8YPHI//6vuB1LLIyKZ6TI5u/19cv6ujSOXqX7989hl6A5O9n+N/SQm4FyE0I29nuvSD9Tv3nuBqiHwAYAAACoRRv1/EvTFNl0vvLBpcWVpTHwgJf+XNxcBF5s/3x15/9NvdLPR9rJRmTTP/Ocv5ceBXa6/W9ud1/+5/bPJkQ4bJ65m+39WGQDUAGBDQAAAFCDNuraxj0txHe+dpism7HW8oH//2Z7pUXef23/XBhbVUQ2MIyb/309FOfsApzFjfvxqbFV5fTG/VhkAzBzAhsAAABg7tqoK65J0r+m/c1XD7O12l7LO/97E9eLuym0WxjVrIlsYHy7//bu3o939+Kftvdi0c28iWwAKiGwAQAAAOasjfrimmT3L6mXfgSgKqvtdXOXhd3uNrtF3saYZne/F9lAeS4f+G9yced+fGJMsyKyAaiAwAYAAACYqzbqjGt2fg2BDXB/kbeJzcLur2GBdy52kc3/hkVdKNnyzrPZ6Z37MdOXvtNPYSdJgNn6HyMAAAAAZqiNuuOa5LUfA+ABq/XVra836+v/rq+f19d52P1k6naRjWAKpiPddz/HZseT/7O9L3/e3qfxHgJAgQQ2AAAAwNy04S+1kyYcBQM8Ly3wnsUmtEk7oKR/dX9hLJO0O55EZAPTlO69H7b34p+3/7f40fsIAAUR2AAAAABz0oa/zL7JLjbAS6zi9u42YpvpEdnAPOx2t9nFj2Ib7yUAFEBgAwAAAMxFG/4S+65fjQA40FXcj22WxjIJu8gGmIdV3I5tzsMxUlN6PzkzBoD5ENgAAAAAc9CGuOYhi7CLAXC8XWzzS1jcnYpTvxdhltK99yyuj5HqtvdoyvVx+64CwAwIbAAAAICpa8Mi4lMcEwX0aRXXi7tpdxtHSPn9CIwjHRn12/Z+/Fs4QqpkX0NkAzALAhsAAABgytqwePicV0YAZJLimhTZ2NWm7N+T740BZm23y9jPcb2rDeX5FJvdxQCYMIENAAAAMFVtiGv2YQcbILdVXO9qYxeF8nwKOydALXa72vzfED6WJh3b+meIbAAmTWADAAAATFEb4pp9pb/MF9kAQ+lis4PCL+traRzFSL8zLepCPdKuNmdxHT6ujKSY5/Kv2z8BmCCBDQAAADA1bYhrXsoxUcDQlrGJbNLibmccRbBzAtSp296L34TwsQSn2/sxABMksAEAAACmpA1xzSHsYAOMZRWb3ROENuOzcwLU7SI24aMdxsZ36p0GYJoENgAAAMBUtOEvog/VhF0LgHGtQmhTgvS74JsxQNWWIbQp5d3mzBgApkVgAwAAAExBG+KaYy2MACjAKq5DmwvjGO33gd+pwDKuQ5uVcYziY9hpEmBSBDYAAABA6dqwENiHt0YAFGS1vt6EHRTG/N3aGgOwvQen6PG3ENqMIb3n2GkSYCIENgAAAEDJ2hDX9CX9xf2JMQCFWcYmskmxzco4BmVRF7ipW18/r6/z9XVlHINJz+ffPKcDTIPABgAAAChVG+KavtmCHihVOi4q7aBgYXdYf4ZFXeBauv+ebe/HnXEMpolNZANA4QQ2AAAAQInaENfk8KsRAIU7i80OChdGMYgU1/xpDMAdKbRJR0alHcYujWMQi/X1yRgAyiawAQAAAErThrgmF7sUAFOwis2RUb+EY6OGkI6JsqgLPGQZm+jxQ9hdbAjvw46TAEUT2AAAAAAlaUNck0v618dvjAGYkGVcHxtFXu+3v4MBHvJ5ez+2u1h+6V2oMQaAMglsAAAAgFK0Ia7JJcU1aScI//IYmKKz2OygsDSKrNIuNqfGADwiPUe+2V6eKfNJO05+MwaAMglsAAAAgBK0Ia7JRVwDzOle5piSfE62v4sdJwg8Je1iYzebvBzdB1AogQ0AAAAwtjbENbmIa4C5SceU/Ly9v9E/i7rAPuxmk186uu+1MQCURWADAAAAjKkNcU0u4hpgrlaxiWzOjSLb72aLusA+7GaTV3pPaowBoBwCGwAAAGAsbYhrchHXADU4i01oszKK3lnUBfa1283mN8+evUtH9n0zBoByCGwAAACAMbQhrslFXAPUds9LkU1nFL068XsaeKFu+wzqCL9+paP7zowBoAwCGwAAAGBobVi0y0VcA9Qo3fN+C7sn9G0RFnWBlz+Lpujxs1H06uP2ngzAyAQ2AAAAwJDaENfkIq4BateF3RP6lhZ1T40BeKEPsTk2ynNpf9I71IkxAIxLYAMAAAAMpQ1xTS7iGoDb98MLo+iNRV3gEBcheuxTE5voEYARCWwAAACAIbQhrslFXANwW7ofpp0TPhhFL9IONhZ1gWOeU0WP/Xi/vl4bA8B4BDYAAABAbm2Ia3IR1wA87rN7ZG/Sou7CGIAD7KLHc6PohV3FAEYksAEAAAByakNck4u4BuB5y3BESV8s6gLHOItNaOPZ9Tgn3q8AxiOwAQAAAHJpw1/+5iKuAXj5PXNpFEdpwlFRwHEuPMP24nU4KgpgFAIbAAAAIIc2xDW5iGsAXu5qe+/sjOIojooC+niW/d+ws9ixPoVdxQAGJ7ABAAAA+taGuCaXbn39HOIagEP9tr7OjeEofscDx9pFjxdGcbAm7CoGMDiBDQAAANCnNiy85dLFZmEYgOOcuZ8epdnOEOAYKbJ5E3YWO4ZdxQAGJrABAAAA+tKGuCaXLiwGA+S4r9oR7DBp14TGGIAepHvxZ2M4mPcvgAEJbAAAAIA+tOEvd3PpQlwDkOv+mo4oEdkcxu99oC8fPO8erAm7igEMRmADAAAAHKsNi2y5dGGxASCnyxDZHGqxfQYA8Nw7rndhVzGAQQhsAAAAgGO0Ia7JpQuLDABDENkc7tP6OjEGwPPvqE68kwEMQ2ADAAAAHKoNf5GbSxcWFwCGJLI5TFrU/WgMgOfg0S3W12tjAMhLYAMAAAAcog1xTS5dWFQAGIPI5jDv19epMQCeh0f3yQgA8hLYAAAAAC/Vhrgmly4sJgCMSWRzGIu6gOfi8TXr68wYAPIR2AAAAAAv0Ya4JpcuLCIAlEBk83KLcDQJ4Pm4BO9iE9oAkIHABgAAANhXG+KaXLqweABQEpHNy9nFBvCcPL6T9fXRGADyENgAAAAA+2hDXJNLFxYNAEoksnmZJhxNAnheLuXd7dQYAPonsAEAAACe04a4JpcuLBYAlCxFNh+MYW/paJITYwAyPTd3xrA3u4oBZCCwAQAAAJ7Shrgmly7ENQDu1/PiaBIgp99CZLOvxfYCoEcCGwAAAOAxbYhrcunCYi3A1O7b58awl/exOS4KIIf0DL00hr14lwPomcAGAAAAeEgb/kI2ly7ENQBTdBZ2TtiXXWyAnN7E5gg/ntZs3+sA6InABgAAALirDXFNLl2IawCmzM4J+z9LnBoDkMnV+vpl+ydPEzwC9EhgAwAAANzUhrgmly7ENQBzYOeE/XwyAiAjkc1+mtgc3QdADwQ2AAAAwE4b4ppcuhDXAMzF1faeblH3aYvtBZBLih0/GMOz0i42J8YAcDyBDQAAAJC0Ia7JpQtxDcDcpEXdN8bwLEeTAEM8a58bw5NSXGMXG4AeCGwAAACANsQ1uXQhrgGYq2XYOeE5i7CLDZDf2fq6MIYnvQu72AAcTWADAAAAdWtDXJNLF+IagLn7HBZ1n2MXG2AI6bl7ZQyPsosNQA8ENgAAAFCvNsQ1uXQhrgGoRbrfXxrDoxZhFxsgv6vYHN13ZRSPsosNwJEENgAAAFCnNsQ1uXQhrgGoydX2vm9R93F2sQGGkGJHR/c97mT7HgjAgQQ2AAAAUJ82xDW5dCGuAaiRRd2nLdZXYwzAQM/jnTE86p0RABxOYAMAAAB1aUNck0sX4hqA2n8PdMbwKLvYAENJwaOj+x7WhF1sAA4msAEAAIB6tCGuyaULcQ0Am0XdlTE8+hzSGAMwgCvP5k8SPAIcSGADAAAAdWhDXJNLF/4CH4CNtKj7xhgeZVEXGIqj+x7XrK/XxgDwcgIbAAAAmL82xDW5dCGuAeC2tKh7bgwPSgu6J8YADOTz+loaw4PeGQHAywlsAAAAYN7aENfk0oW4BoCHncUmtOG2FNe8NwZgQOl5/coY7llsLwBeQGADAAAA89WGuCaXLsQ1ADzNou7D3hoBMKCV53b3Y4C+CGwAAABgntoQ1+TShb+kB+B5jop6WLN9TgEYysX24v47Y2MMAPsT2AAAAMD8pKMXxDV5dCGuAWB/n8NRUQ95ZwTAwOwq9rDWCAD2J7ABAACAeUlhzSdjyKILcQ0AL+d3x32n2wtgKCmusavYfYJHgBcQ2AAAAMB8pLimNYYsurBACsBhHBX1MIu6wNDSrmJLY7jlxDskwP4ENgAAADAP4pp8uhDXAHCcs/W1MoZb0nPLiTEAA/Ncf99bIwDYj8AGAAAApk9ck08X/hIegH74fXKf5xdgaKuwq9hdi3BsH8BeBDYAAAAwbeKafLqwGApAf5br68IYbnFMFDCGdFTUyhjcjwFeSmADAAAA0yWuyacLcQ0A/fuwvq6M4YcmNjsnAAzpans/5tprIwB4nsAGAAAApklck08X4hoA8litry/GcMtbIwBGkHYUWxrDDyfeLwGeJ7ABAACA6RHX5NOFuAaAvM7C0SQ3pV0TTowBGIHn/tsEjwDPENgAAADAtIhr8unCX7IDMIxzI/ghxTWOJgHGsFpfn43hh0Vsju4D4BECGwAAAJgOcU0+XYhrABj2987SGH6wawIwlhQ8XhnDD943AZ4gsAEAAIBpENfk04W4BoDh2cXm2iLsmgCMI8U1X4zhB8EjwBMENgAAAFA+cU0+XYhrABjHMuxic5NjooCxpGOiVsbwXbO+To0B4GECGwAAACibuCafLsQ1AIzL76Fr74wAGEnaxcauYtfsYgPwCIENAAAAlEtck08XFjUBGN9q+zsJuyYA478frIzhO++gAI8Q2AAAAECZxDX5dCGuAaAcdk24ZtcEwP14fCfh2D6ABwlsAAAAoDzimny6ENcAUJZV2MVmx4IuMPa7wsoYvvvVCADuE9gAAABAWcQ1+XQhrgGgTHZN2GjCMVGA+3EJBI8ADxDYAAAAQDnENfl0Ia4BoFyrsIvNjmOigLHfG1bG4JgogIcIbAAAAKAM4pp8uhDXAFC+L0bwnQVdYGx2sdlwTBTAHQIbAAAAGJ+4Jp8uxDUATMPl+loag2OigNFdrK8rY4iFEQDcJrABAACAcYlr8ulCXAPAtNg1YWNhBMCIUlxjVzHBI8A9AhsAAAAYj7gmny7ENQBMz3J9rYwh3hoBMLLPRuB+DHCXwAYAAADGIa7JpwtxDQDTZRebzY4JJ8YAjOhq+15Ru4URAFwT2AAAAMDwxDX5dCGuAWD6v8uujCFeGwEwMsdEbYLHxhgANgQ2AAAAMCxxTT5diGsAmAeLuhG/GgEwssvYHN1Xu4URAGwIbAAAAGA44pp8uhDXADCv32u1WxgBUIDfjUDwCLAjsAEAAIBhiGvy6UJcA8C8rNbXReUzOAmRDVDGu0btx/a5FwNsCWwAAAAgP3FNPl2IawCYJ7smWNQFynnnqJngEWBLYAMAAAB5iWvy6UJcA8B8pR1sVpXPwLEkQAm+GIHABiAR2AAAAEA+4pp8uhDXADB/tR8TdRqbnRMAxrRaX8vKZyB4BAiBDQAAAOQirsmnC3ENAHWwa4JdE4Ay1H5sn+ARIAQ2AAAAkIO4Jp8uxDUA1GO1vi4rn8ErPwZAAS6MQPAIILABAACAfolr8ulCXANAfWrfNWHhRwAowNX2faRmgkegegIbAAAA6I+4Jp8uxDUA1Ps7sGaOJQFK8Yf7MUDdBDYAAADQD3FNPl2IawCoV9o1ofajSRZ+DIACXGzvye7FAJUS2AAAAMDxxDX5dCGuAQC7JgCUQfAIUDGBDQAAABxHXJNPF+IaAEhqX9B95UcAKETtwePCjwBQM4ENAAAAHE5ck08X4hoA2ElHkiwr/vwLPwJAIWo/JuonPwJAzQQ2AAAAcBhxTT5diGsA4C7HRAGUoeZdxRa+fqBmAhsAAAB4OXFNPl2IawDgIbUfE7XwIwAU4q+KP/vJ+mr8CAC1EtgAAADAy4hr8ulCXAMAj1mtr8uKP79jSYBS1B482lEMqJbABgAAAPYnrsmnC3ENADxnWfFnt6ALlOKq8vvxKz8CQK0ENgAAALAfcU0+XYhrAGAff1T82QU2gPux+zHAqAQ2AAAA8DxxTT5diGsAYF/L2OycUKuFHwGgoPtxrQQ2QLUENgAAAPA0cU0+XYhrAOCllhV/dou6QCkuo97g8WR9NX4EgBoJbAAAAOBx4pp8uhDXAMAh/qr4s//L1w8UZFnxZxc8AlUS2AAAAMDDxDX5dCGuAYBDLSv+7BZ0gZLUHDy6HwNVEtgAAADAfeKafLoQ1wDAMWo+lsSCLlCSZcWf/SdfP1AjgQ0AAADcJq7JpwtxDQD0YVnp5z5ZX42vHyhEzcGjezFQJYENAAAAXBPX5NOFuAYA+lLzsSSNrx8oyLLSz21HMaBKAhsAAADYENfk04W4BgD6dFnxZ7eoC5Sk5uDR/RiojsAGAAAAxDU5dSGuAYC+LSv+7P/y9QMFqTl4bHz9QG0ENgAAANROXJNPF+IaAMhlWenntmMC4F7sfgwwCoENAAAANRPX5NOFuAYAcqp114TGVw+4HxfBjmJAdQQ2AAAA1Epck08X4hoAyO3flX7uxlcPFEbwCFAJgQ0AAAA1Etfk04W4BgCGcFnxZ3csCVCSWoNH92KgOgIbAAAAaiOuyacLcQ0ADKXmwObE1w+4H7sXAwxNYAMAAEBNxDX5dCGuAYCh1bqoa9cEoCTLij+7+zFQFYENAAAAtRDX5NOFuAYAxrCq9HPbNQFwP3Y/BhicwAYAAIAaiGvy6UJcAwBj+Xeln/snXz1QmFWln9sONkBVBDYAAADMnbgmny7ENQAwplqPiLJjAlCav9yPAeZPYAMAAMCciWvy6UJcAwBjW1X6uRtfPVCYq0o/97989UBNBDYAAADMlbgmny7ENQBQglp3sGl89YD7sfsxwNAENgAAAMyRuCafLsQ1AFCSlREAuBcDkJ/ABgAAgLkR1+TThbgGAEqzqvRzL3z1gHuxezHAkAQ2AAAAzIm4Jp8uxDUAUKKVEQAU4dIIAOZNYAMAAMBciGvy6UJcAwCl+m+ln/vEVw8U5soIAOZNYAMAAMAciGvy6UJcAwAlq3VB99RXDxRmVennXvjqgVoIbAAAAJg6cU0+XYhrAKB0jiQBKMN/jQBg3gQ2AAAATJm4Jp8uxDUAAAAA8J3ABgAAgKkS1+TThbgGAKai1h1sfvLVA4VZVvq5G189UAuBDQAAAFMkrsmnC3ENAEzJVaWf+8RXD1CExgiAWghsAAAAmBpxTT5diGsAAAAOcWUEAPMmsAEAAGBKxDX5dCGuAYCpujQCAPdiAPIS2AAAADAV4pp8uhDXAMCU2TUBAAAyE9gAAAAwBeKafLoQ1wAA07MwAoAi/GQEQC0ENgAAAJROXJNPF+IaAACAvtS4o9iJrx2ohcAGAACAkolr8ulCXAMAc7EyAoAiXBoBwHwJbAAAACiVuCafLsQ1ADAn/zUCAADIS2ADAABAicQ1+XQhrgEAAACAFxHYAAAAUBpxTT5diGsAAAAA4MUENgAAAJREXJNPF+IaAAAAADiIwAYAAIBSiGvy6UJcAwAAAAAHE9gAAABQAnFNPl2IawAAAADgKAIbAAAAxiauyacLcQ0AAAAAHE1gAwAAwJjENfl0Ia4BAAAAgF4IbAAAABiLuCafLsQ1AAAAANAbgQ0AAABjENfk04W4BgAAAAB6JbABAABgaOKafLoQ1wAAAABA7wQ2AAAADElck08X4hoAAAAAyEJgAwAAwFDENfl0Ia4BAAAAgGwENgAAAAxBXJNPF+IaAKjdP40AAADyEtgAAACQm7gmny7ENQBAxKkRALgfA5CXwAYAAICcxDX5dCGuAQAAKMmJEQDMl8AGAACAXMQ1+XQhrgEA6nZpBABF+MsIgFoIbAAAAMhBXJNPF+IaAIArIwAAYEgCGwAAAPomrsmnC3ENAHDfwggARndqBADzJrABAACgT+KafLoQ1wAAAJTqxAgA5k1gAwAAQF/ENfl0Ia4BAACgPI7sA6ohsAEAAKAP4pp8uhDXAACPW1T6uf/y1QOFqfWIqEtfPVALgQ0AAADHEtfk04W4BgAAYAocEQUwcwIbAAAAjiGuyacLcQ0A8LzGCACK8E8jAJg3gQ0AAACHEtfk04W4BgDYT1Pp53YkCVAaR0QBzJzABgAAgEOIa/LpQlwDAOyv1h0Trnz1QGFqPSLK/RiohsAGAACAlxLX5NOFuAYAeJlTIwBwPwYgP4ENAAAALyGuyacLcQ0A8HJNpZ/bkSRASWrdvWbpqwdqIrABAABgX+KafLoQ1wAAh2kq/dyOJAFKYvcagAoIbAAAANiHuCafLsQ1AMBhal3QFdcApWkq/dwrXz1QE4ENAAAAzxHX5NOFuAYAOFytR5I4HgooTVPp5/6vrx6oicAGAACAp4hr8ulCXAMAHGdhBABF+KnSz21HMaAqAhsAAAAeI67JpwtxDQBwvH9V+rn/8tUDhWkq/dx2FAOqIrABAADgIeKafLoQ1wAA/WiMAKAIp0YAMH8CGwAAAO4S1+TThbgGAOjPotLPvfTVA+7F7scAQxPYAAAAcJO4Jp8uxDUAQH/slgBQhqbSz33lqwdqI7ABAABgR1yTTxfiGgCgX03Fn33p6wcK8lOln/vSVw/URmADAABAIq7JpwtxDQDQv1p3sLFjAuB+7H4MMAqBDQAAAOKafLoQ1wAAebyq9HPbMQEozaLSz/1vXz1QG4ENAABA3cQ1+XQhrgEA8llU+rlXvnqgIKcVf3bBI1AdgQ0AAEC9xDX5dCGuAQDyqXlB97++fqAgi4o/uyOigOoIbAAAAOokrsmnC3ENAJDXouLPvvT1AwX5yf0YoB4CGwAAgPqIa/LpQlwDAORX84LuytcPFGThXgxQD4ENAABAXcQ1+XQhrgEAhrGo+LOvfP1AIZrt5V4MUAmBDQAAQD3ENfl0Ia4BAIbRRL0LuktfP1CQRcWf/S9fP1AjgQ0AAEAdxDX5dCGuAQCGs6j4s698/UBBXrkfA9RFYAMAADB/4pp8uhDXAADDqnlB99++fqAgi4o/+6WvH6iRwAYAAGDexDX5dCGuAQCG97riz25BFyhFE/Ue1+d+DFTrH0YAAAAwW+KafFJY0xkDADCw0/V1UvHnt6ALlKLm2HHp6wdqZQcbAACAeRLX5COuAQDGsqj4s6/W15UfAaAQNR/XJ3YEqiWwAQAAmB9xTT7iGgBgTG8r/uwWdIGS1LyDzb99/UCtBDYAAADzIq7JR1wDAIwpHQ11WvHn/8uPAFCI15V/fsEjUC2BDQAAwHyIa/IR1wAAY7OgC1CGX92PAeoksAEAAJgHcU0+4hoAoAS1L+gu/QgAhVi4FwPUSWADAAAwfeKafMQ1AEApat7Bxm4JQCnSUX1NxZ/fcX1A1QQ2AAAA0yauyUdcAwCUovbjoZZ+BIBCvK388wsegaoJbAAAAKZLXJOPuAYAKEntx0PZMQEoheARoGICGwAAgGkS1+QjrgEASnISFnTtmACUoPbjodK9+MqPAVAzgQ0AAMD0iGvyEdcAAKVJcc1JxZ9/tb0Axvau8s+/9CMA1E5gAwAAMC3imnzENQBAiWo/HmrpRwAoRO27if3bjwBQO4ENAADAdIhr8hHXAAAlcjxUxF9+DIAC1L6bWHLhxwConcAGAABgGsQ1+YhrAIBSef6zgw1QhreVf/7L9XXlxwConcAGAACgfOKafMQ1AEDJ3lX++dOC7sqPATAyu4mJHQG+E9gAAACUTVyTj7gGACjZ6fpqKp/B0o8BUADv5I7rA/hOYAMAAFAucU0+4hoAoHTvjMCCLuB+XIilEQAIbAAAAEolrslHXAMAlM5xJBtLIwBGtgi7iaV78ZUfBQCBDQAAQInENfmIawCAKUjPgieVz2AZFnSB8b01gvjDCAA2BDYAAABlEdfkI64BAKbCcSQWdIHxNd7Pv1saAcCGwAYAAKAc4pp8xDUAwFQswnEkyYURACPzfh6xWl+XxgCw8Q8jAAAAGF3a/v9bbBZT6J+4BgCYko9G8H1Bd2UMwMjsJiZ2BLhFYAMAADCuFNf8ub5OjSILcQ0AMCVNiK4TC7rA2Nrt+3rt/jICgGuOiAIAABiPuCYvcQ0AMDV2r9n43QgA9+PRXYXgEeAWgQ0AAMA4xDV5iWsAgKlpYrNjQu3Sgu6lMQAjarf35NqJawDuENgAAAAMT1yTl7gGAJiid0bwnQVdYGxvjeA7x0MB3CGwAQAAGJa4Ji9xDQAw1WfE1hi++8MIgBEttheCR4B7BDYAAADDEdfkJa4BAKbq/fZZsXbpeCgLusCYPhrBdxfbezIANwhsAAAAhiGuyUtcAwBM+TnR8VAb4hpgTIuwe82O3cQAHiCwAQAAyE9ck5e4BgCYMrvXXLOgC4zJ7jUbdhMDeITABgAAIC9xTV7iGgBg6s+Kdq/ZsKALjGkRdq/ZcTwUwCMENgAAAPmIa/IS1wAAU/cp7F6z47kOGPt+zIbdxAAeIbABAADIQ1yTl7gGAJi6Zn21xvDD70YAjKT17v6D3cQAniCwAQAA6J+4Ji9xDQAwBx+N4IfV+ro0BsD9eHTetQGeILABAADol7gmL3ENADAHi7B7zU1fjAAYyVlsdhRjw25iAE8Q2AAAAPRHXJOXuAYAmAu7JdzmOBJgrHf4d8bwwyrsJgbwJIENAABAP8Q1eYlrAIC5aGOzgw0bKa5ZGQMwgo/bd3k27CYG8AyBDQAAwPHENXmJawCAOT032r3mtj+MABhBen9/bwy3eO8GeIbABgAA4DjimrzENQDAnKTF3MYYfrjyrAeM5JMR3NJt78kAPEFgAwAAcDhxTV7iGgBgTpqwe81dnvWAMbThqL67fjcCgOcJbAAAAA4jrslLXAMAzM1XI7jnixEAI7zL273mttX6WhoDwPMENgAAAC8nrslLXAMAzE0bdku4axmbRV2AIX3cvtNzTewIsCeBDQAAwMuIa/IS1wAAc3x+tFvCfRZ0gaEt1td7Y7jHOzjAngQ2AAAA+xPX5CWuAQDm6GvYLeGu1fq6MAZg4Pd5R/Xdl97Br4wBYD8CGwAAgP2Ia/IS1wAAc/R6e3Gb3WuAoaWdaxpjcD8GOIbABgAA4HnimrzENQDAXJ8h7ZZw35VnP2Bg6V3+ozHcs1xfl8YAsD+BDQAAwNPENXmJawCAuXI01MPS0VCOIwGGfKf/ZgwPsnsNwAsJbAAAAB4nrslLXAMAzJWjoR53bgTAgNLONY0x3LOKTfAIwAsIbAAAAB4mrslLXAMAzPk50tFQD0uLuStjAAayWF/vjeFBYkeAAwhsAAAA7hPX5CWuAQDm7Fs4GuoxjiMBhnyvdzTUw9IxfXavATiAwAYAAOA2cU1e4hoAYM7STgkLY3jQcnsBDOFriB0fk2LHK2MAeDmBDQAAwDVxTV7iGgBgztIz5CdjeJTjSIChpNjxtTE8KIU1n40B4DACGwAAgA1xTV7iGgBg7s+SjiJ53CrsXgMMQ+z4NLvXABxBYAMAACCuyU1cAwDMXTqKpDGGR9m9Bhjq3V7s+DTv5gBHENgAAAC1E9fkJa4BAObuLBxF8pSV50FgICmuaYzhUd32ngzAgQQ2AABAzcQ1eYlrAIC5W6yvj8bwJLvXAEM4296TcT8GyEZgAwAA1Epck5e4BgCYuyYcRfKclWdCYABpFzGx49O6sHsNwNEENgAAQI3ENXmJawCAGp4nv23/5HF2SwByS+/1X43B/RhgCAIbAACgNuKavMQ1AEANvnqefNbKcyEwwPu92PF552H3GoBeCGwAAICaiGvyEtcAADX4FJvjSHjaByMAMkvv940xPOlqfX02BoB+CGwAAIBaiGvyEtcAADVo19d7Y3jWcn1dGAOQkZ3E9vMlNpENAD0Q2AAAADUQ1+QlrgEAapB2rflqDHs5NwIgo7STWGsMz7J7DUDPBDYAAMDciWvyEtcAADVIz5Limv0stxdADm3YSWxfKXa0ew1AjwQ2AADAnIlr8hLXAAA1ON0+U54Yxd7PiAA5tCF23Ncq7F4D0DuBDQAAMFfimrzENQBALc+U30Jcs6/0fLgyBiADO4m9zAcjAOifwAYAAJgjcU1e4hoAoKZnysYo9pKOIbGgC+Sw20mM/SzX14UxAPRPYAMAAMyNuCYvcQ0A4JmSh3yJTWQD0CfH9L2c2BEgE4ENAAAwJxZC8hLXAACeKXnIan2dGQPQM3HNy6V39ktjAMhDYAMAAMyFhZC8xDUAgGdKHmO3BKBv4pqXc1QfQGYCGwAAYA4shOQlrgEAPFPymOX6ujAGoEfimsOch6P6ALIS2AAAAFNnISQvcQ0A4JmS554XAfoirjlMOhbqszEA5CWwAQAApsxCSF7iGgDAMyVPSbslrIwB6Im45nCOhgIYgMAGAACYKgsheYlrAADPlDxlFXZLAPojrjlcendfGgNAfgIbAABgiiyE5CWuAQBq0HimPPqZ8coYgB68DnHNodJ92O41AAP5hxEAAAATI67JS1wDANTATgnHuQi7JQD9aNfXV2M4WIprxI4AA7GDDQAAMCXimrzENQBADcQ1x7naPjcCHOt9iGuOsfQODzAsgQ0AADAV4pq8xDUAQA3a9fV3iGuOcR52SwCOl8KaT8ZwMLEjwAgENgAAwBSIa/IS1wAANTgLOyUca7m+PhsDcOT7/bfYBI8cLsWOK2MAGNY/jAAAACicuCYvcQ0AUMPzZNoloTWKo9gtAThWE5u4xvv9cS5D7AgwCoENAABQMnFNXuIaAMDzJPuyWwJwjNPt/dgRff28ywMwAkdEAQAApbIYkpe4BgCYu/Qc+R/Pk71Yht0SgMO16+vvENf04UNsdrABYAQCGwAAoETimrzENQDA3LVhMbcvjoYCjnm3/7q9ON4yxI4Ao3JEFAAAUBpxTV7iGgBg7s+Sn2IT2NCPtFvCyhiAF2rW1zfv9r0ROwIUwA42AABAScQ1eYlrAIA5O90+S7ZG0ZsLz4/AAV7HZhcx7/b9ETsCFEBgAwAAlEJck5e4BgCYs9azZO9WYbcE4OXSLmLfwhF9fRI7AhTCEVEAAEAJxDV5iWsAgDk/RzoSKt8z5JUxAHtK7/Nfvdf3ztFQAAWxgw0AADA2cU1e4hoAYK4WsTmCpDWK3p2vr6UxAHt6770+mzchdgQohh1sAACAMYlr8hLXAABzdba+PhpDFpfb+QLs806fjoNaGEUWYkeAwghsAACAsYhr8hLXAABz5AiSvNIuCW+MAdjD6+39+MQosliG2BGgOI6IAgAAxiCuyUtcAwDM0VlsjoTyDJn3OXJlDMAz7/Pftpe4Jg+xI0Ch7GADAAAMTVyTl7gGAJibRWx2SWiMIqvP6+vCGIAnvI/N8XzCmrxSXHNlDADlsYMNAAAwJHFNXuIaAGBuz46fts+PjXFktVxfH4wBeMTp9l78KcQ1uZ1v78kAFMgONgAAwFDENXmJawCAOWnDQu5QHEUCPPUev9u1hvzSLmJnxgBQLoENAAAwBHFNXuIaAGAu0vNiCmsWRjGYX8JRJMB9bWzCmsYoBrHavtsDUDCBDQAAkJu4Ji9xDQAwl2fGFNa0RjH4s+SlMQA3LGIT1iyMYjC7ncTEjgCFE9gAAAA5iWvyEtcAAHN4XkzHj7wLx0ENrfMsCdzQxCasaY1icB9C7AgwCQIbAAAgF3FNXuIaAGDq2tjsWiOsGd5lOIoEuH53T6HjR6MYxbl3e4DpENgAAAA5iGvyEtcAAFPWxmYhtzGKUazW1y/GAN7bww5iY7tYX2fGADAdAhsAAKBv4pq8xDUAwFS1IawZ29X6erP9E6j3nV1YMz47iQFMkMAGAADok7gmL3ENADBFbQhrSnqevDQGqPZ9XVhThhQ5/hJiR4DJEdgAAAB9EdfkJa4BAKb2bPg6hDWlPU9eGANUJ92DU1TThrCmBOIagAkT2AAAAH0Q1+QlrgEApqKJzSKuHRLK0nmehOqcxnVYQ1nv93YSA5gogQ0AAHAscU1e4hoAYAoW6+ttWMgtUbd9pgTq0G7vxwujKPL93k5iABMmsAEAAI4hrslLXAMAlP4smI6Beud5sFhpl4QPxgCz18R1WNMYR5E+e78HmD6BDQAAcChxTV7iGgCgVLtjR16HY6BKluKaX9bXlVHAbLXr69ft/ZhypXd7sSPADAhsAACAQ4hr8hLXAAClaeJ6t5rGOIqXohpxDcyTyHFaunBMH8BsCGwAAICXEtfkJa4BAEp67mtjc+SIZ7/pENfA/Jxu78UpqmmMYzIc0wcwMwIbAADgJcQ1eYlrAICxNbFZwE1HjiyMY3J2cc2lUcDkiWqmzTF9ADMksAEAAPYlrslLXAMAjCU93+2iGs960yWugem7GTg2xjFZ4hqAmRLYAAAA+xDX5CWuAQCG1MRm8fZVbBZzT4xkFt6EuAam5vTO/ZjpE9cAzJjABgAAeI64Ji9xDQCQWxPXC7iLsCvCXJ8pl8YAxTu9cz8WOM7LKsQ1ALMmsAEAAJ4irslLXAMA5LDYXj9tn+MaI/FMCQyu2d6D0/Vq+6egZr5SVPMmxDUAsyawAQAAnvI1xDW5WAgBAI7VxPXuNGIaz5TAeE5v3IPFNPVJUU3aucYxfQAzJ7ABAACe4i8E87AQAgDsa7dIu/szhTRNiKDxTAlDW9z4859xHdQ0RlM1cQ1ARQQ2AAAAw7IQAgB1auLhRdjFjf87xTMnD/zv4JkS+vPQ7jK7kHHn1SP/O9x0ub0fi2sAKiGwAYj4f0YAMBnL2PyrIJgqCyFAKf4Mi/cAU3S1faa8MIos2tgckwvwnBTV/LK9LwNQCYENAADAMMQ1AAAcwzEk+e2e10U2wFPENQCV+h8jAAAAyE5cAwDAMcQ1w+m2z+8ADxHXAFRMYAMAAJCXuAYAgGOIa4bXhcgGuE9cA1A5gQ0AAEA+4hoAAI6RFnN/DnHNGLoQ2QC37wnpfiyuAajYP4wAAAAgC3ENAADHsFPC+HbP81+NAqq/FwjuALCDDQAAQAbiGgAAjnER4ppSdGFhHWr2wT0AgB072AAAAPRLXAMAwDG6sJj7/9m71+O4jSwAo7ccwWaw5RAcgkJQBq0QmIGUAZXBVQbcDMYZjDOAM+BmsIOdhkVJpDQP9AzQOKcK1Sz/bJKoofvT7SV+T0Ym2YC/7wHYMIENAADAfPzPNwAArjFOSni0DYs0fc4X2UD/xulh4xSxva0A4CVXRAEAAMxDXAMAwKWe6+dJcc2yZZguBL0bQlwDwBtMsAEAALieuAYAgEuZlLAu0+d+k2ygP/v6Pn62FQC8xgQbAACA64hrAAC41HiY+3uIa9YmwyQb6PH3+o8Q1wDwEwIbAACAy4lrAAC41Pg50qSEdX//RDbQhwe/zwCcwhVRAAAAlxHXAABwqfEw99E2rN7094DromCdxsDx/eHZ2QoATiGwAQAAOJ+4BgCASzjM7c/0d4HIBtZlX9/Hg60A4FSuiAIAADiPuAYAgEuMh7l/hLimRxmul4G1/c6OV/QNtgKAcwhsAAAATieuAQDgEuNnSIe5/X+PRTawbM/19/RD/RoAzuKKKAAAgNOIawAAONd4gPvgc+RmTN9n10XB8gxxvBJqbysAuJQJNgAAAL8mrgEA4FzjIe47nyM3J8MkG1iapzhe0SeuAeAqAhsAAICfE9cAAHCu8fPjGNc4zN3u919kA/c3XQn1PlwJBcAMXBEFAADwNnENAADnmA5zn2zF5k1/R7guCu5jX9/HQkcAZmOCDQAAwOvENQAAnGMXxytIxDVMMkyygXt4DFdCAdCACTYAAAA/EtcAAHCOhzge6ML3pr8rTLKB9ob69/zOVgDQgsAGAADgW7sQ1wAAcBpXkHCK6e8LkQ2081Tfx8+2AoBWXBEFAAAAAADn+xSuIOF0Ga6LghbGoOZ9fcQ1ADRlgg0AAAAAAJzO1BoulXU1yQbmYWoNADdlgg0AAAAAAJzG1BqulWGSDVzL1BoA7sIEGwAAAAAA+LldHKOIwVYwg6yrSTZwvsc4xo7CGgBuzgQbAAAAAAB43XiA+3B43oW4hnllmGQD59jXd/FDiGsAuBMTbAAAAAAA4EcZDnJp/zM2MskG3ja+gz8fno+2AoB7E9gAAAAAAMBX45SEMazZ2QpuIOsqsoEfPdX38WArAFgCgQ0AAAAAABynJHw6PI+2ghvLuops4GiI4xVqO1sBwJL8ZgsAAAAAANi4PDy/h7iG+/4MfrANbNwYOj7U9/HOdgCwNCbYAAAAAACwVbs4HububQULkHU1yYYtGgPHcYrYs60AYKkENgAAAAAAbM0Qrh9hmbKuIhu2Ylffx4OtAGDpXBEFAAAAAMBWDHE8yHX9CEuW4boo+je+g9/VZ7AdAKyBCTYAAAAAAPRuvHLk8+H5aCtYiayrSTb0Zojj1XxPtgKAtRHYAAAAAADQqymseaxfw5pkXUU29GA4PJ9e/FwDwOoIbAAAAAAA6I2whl5kXUU2rNUQwhoAOiGwAQAAAACgF8IaepR1FdmwJkMIawDojMAGAAAAAIC1Gw7PlxDW0K+sq8iGpdvHMXRMWwFAbwQ2AAAAAACs1RAmJLAd08+5yIYl2tX38c5WANArgQ0AAAAAAGuzi+OEhCdbwcZkXUU2LOlncnwf720FAL0T2AAAAAAAsBYZDnIh6yqy4V6e4+s1UIPtAGArBDYAAAAAACzZcHi+HJ7HOB7qAiIb7mOMGz+Ha/kA2CiBDQAAAAAASzRe//QlXAMFb8m6imy4xc+a6WEAbJ7ABgAAAACApRjiGNVkuHYETpF1Fdkwt2lazRg5mh4GACGwAQAAAADg/vLw/CdMq4FLf39GIhuu9Vzfw6bVAMArBDYAAAAAANyD6Qgwn6yryIZLuJIPAE4gsAEAAAAA4FbGqGY6xB1sB8wq6yqy4RTje3iaHCZyBIATCGwAAAAAAGhJVAO3k3UV2fAaUQ0AXEFgAwAAAADA3EQ1cD9ZV5ENEaIaAJiNwAYAAAAAgGuNh7a7cIgLS5F1Fdlsz/Dd+xgAmInABgAAAACAS4xTanZxPMTd2Q5YnKyryKZ/L9/Fe9sBAG0IbAAAAAAAOMUQ3x7imlIDy5d1Fdn0ZQoc/wxTagDgZgQ2AAAAAAC85uUB7vj1YEtglbKuIpv12tX38J8hcASAuxHYAAAAAAAwHta+PLzdhwNc6EnWVWSzfEN9B/9V38c7WwIAyyCwAQAAAADYlqE+02Qa02lgG7KuIptlvY9fxjTiRgBYMIENAAAAAECfpqk04/P3i68d3sJ2ZV1FNrc1xNeYZnof72wLAKyLwAYAAAAAYL2GF890aDsGNDtbA7wh6yqyaWN8B3/2PgaA/ghsAAAAAACWZ/fi6/GQ9r/xdSLN9N9MogEulXUV2czvX4fn34fno60AgL4IbAAiPtkCgNUYbAEAcMXniC+2gQX8HL71mXZne4Aby7qKbOZX6vrBVgBAPwQ2AP4lAQAAwBYM/v4DgB9kXUU28yt1FdkAQCd+swUAAAAAAACblSECaaWEeAkAuiGwAQAAAAAA2LYMkU0rJUQ2ANAFgQ0AAAAAAAAZIptWSohsAGD1BDYAAAAAAACMMkQ2rZQQ2QDAqglsAAAAAAAAmGSIbFopIbIBgNUS2AAAAAAAAPBShsimlRIiGwBYJYENAAAAAAAA38sQ2bRSQmQDAKsjsAEAAAAAAOA1GSKbVkqIbABgVQQ2AAAAAAAAvCVDZNNKCZENAKyGwAYAAAAAAICfyRDZtFJCZAMAqyCwAQAAAAAA4FcyRDatlBDZAMDiCWwAAAAAAAA4RYbIppUSIhsAWDSBDQAAAAAAAKfKENm0UkJkAwCLJbABAAAAAADgHBkim1ZKiGwAYJEENgAAAAAAAJwrQ2TTSgmRDQAsjsAGAAAAAACAS2SIbFopIbIBgEUR2AAAAAAAAHCpDJFNKyVENgCwGAIbAAAAAAAArpEhsmmlhMgGABZBYAMAAAAAAMC1MkQ2rZQQ2QDA3QlsAAAAAAAAmEOGyKaVEiIbALgrgQ0AAAAAAABzyRDZtFJCZAMAdyOwAQAAAAAAYE4ZIptWSohsAOAuBDYAAAAAAADMLUNk00oJkQ0A3JzABgAAAAAAgBYyRDatlBDZAMBNCWwAAAAAAABoJUNk00oJkQ0A3IzABgAAAAAAgJYyRDatlBDZAMBNCGwAAAAAAABoLUNk00oJkQ0ANCewAQAAAAAA4BYyRDatlBDZAEBTAhsAAAAAAABuJUNk00oJkQ0ANCOwAQAAAAAA4JYyRDatlBDZAEATAhsAAAAAAABuLUNk00oJkQ0AzE5gAwAAAAAAwD1kiGxaKSGyAYBZCWwAAAAAAAC4lwyRTSslRDYAMBuBDQAAAAAAAPeUIbJppYTIBgBmIbABAAAAAADg3jJENq2UENkAwNUENgAAAAAAACxBhsimlRIiGwC4isAGAAAAAACApcgQ2bRSQmQDABcT2AAAAAAAALAkGSKbVkqIbADgIgIbAAAAAAAAliZDZNNKCZENAJxNYAMAAAAAAMASZYhsWikhsgGAswhsAAAAAAAAWKoMkU0rJUQ2AHAygQ0AAAAAAABLliGyaaWEyAYATiKwAQAAAAAAYOkyRDatlBDZAMAvCWwAAAAAAABYgwyRTSslRDYA8FMCGwAAAAAAANYiQ2TTSgmRDQC8SWADAAAAAADAmmSIbFopIbIBgFcJbAAAAAAAAFibDJFNKyVENgDwA4ENAAAAAAAAa5QhsmmlhMgGAL4hsAEAAAAAAGCtMkQ2rZQQ2QDAPwQ2AAAAAAAArFmGyKaVEiIbAPg/gQ0AAAAAAABrlyGyaaWEyAYABDYAAAAAAAB0IUNk00oJkQ0AGyewAQAAAAAAoBcZIptWSohsANgwgQ0AAAAAAAA9yRDZtFJCZAPARglsAAAAAAAA6E2GyKaVEiIbADZIYAMAAAAAAECPMkQ2rZQQ2QCwMQIbAAAAAAAAepUhsmmlhMgGgA0R2AAAAAAAANCzDJFNKyVENgBshMAGAAAAAACA3mWIbFopIbIBYAMENgAAAAAAAGxBhsimlRIiGwA6J7ABAAAAAABgKzJENq2UENkA0DGBDQAAAAAAAFuSIbJppYTIBoBOCWwAAAAAAADYmgyRTSslRDYAdEhgAwAAAAAAwBZliGxaKSGyAaAzAhsAAAAAAAC2KkNk00oJkQ0AHRHYAAAAAAAAsGUZIptWSohsAOjE/wRg796O5DiSNIz+D6vIarAUASJQAx8RqMGMBDsiuAgUgaNBiwARsBosCmiQANEI9KW8Ki/nmIXVu2c9uFl+liGwAQAAAAAA4Ow6IpspFZENAAcgsAEAAAAAAACRzaSKyAaAnRPYAAAAAAAAwGcdkc2UisgGgB0T2AAAAAAAAMBfOiKbKRWRDQA7JbABAAAAAACAb3VENlMqIhsAdkhgAwAAAAAAAN/riGymVEQ2AOyMwAYAAAAAAACe1hHZTKmIbADYEYENAAAAAAAA/FhHZDOlIrIBYCcENgAAAAAAALDWEdlMqYhsANgBgQ0AAAAAAAD8XEdkM6UisgFg4wQ2AAAAAAAA8Dwdkc2UisgGgA0T2AAAAAAAAMDzdUQ2UyoiGwA2SmADAAAAAAAAL9MR2UypiGwA2CCBDQAAAAAAALxcR2QzpSKyAWBjBDYAAAAAAADwOh2RzZSKyAaADRHYAAAAAAAAwOt1RDZTKiIbADZCYAMAAAAAAABv0xHZTKmIbADYAIENAAAAAAAAvF1HZDOlIrIB4M4ENgAAAAAAAHAdHZHNlIrIBoA7EtgAAAAAAADA9XRENlMqIhsA7kRgAwAAAAAAANfVEdlMqYhsALgDgQ0AAAAAAABcX0dkM6UisgHgxgQ2AAAAAAAAMKMjsplSEdkAcEMCGwAAAAAAAJjTEdlMqYhsALgRgQ0AAAAAAADM6ohsplRENgDcgMAGAAAAAAAA5nVENlMqIhsAhglsAAAAAAAA4DY6IpspFZENAIMENgAAAAAAAHA7HZHNlIrIBoAhAhsAAAAAAAC4rY7IZkpFZAPAAIENAAAAAAAA3F5HZDOlIrIB4MoENgAAAAAAAHAfHZHNlIrIBoArEtgAAAAAAADA/XRENlMqIhsArkRgAwAAAAAAAPfVEdlMqYhsALgCgQ0AAAAAAADcX0dkM6UisgHgjQQ2AAAAAAAAsA0dkc2UisgGgDcQ2AAAAAAAAMB2dEQ2UyoiGwBeSWADAAAAAAAA29IR2UypiGwAeAWBDQAAAAAAAGxPR2QzpSKyAeCFBDYAAAAAAACwTR2RzZSKyAaAFxDYAAAAAAAAwHZ1RDZTKiIbAJ5JYAMAAAAAAADb1hHZTKmIbAB4BoENAAAAAAAAbF9HZDOlIrIB4CcENgAAAAAAALAPHZHNlIrIBoAFgQ0AAAAAAADsR0dkM6UisgHgBwQ2AAAAAAAAsC8dkc2UisgGgCcIbAAAAAAAAGB/OiKbKRWRDQB/I7ABAAAAAACAfeqIbKZURDYAfEVgAwAAAAAAAPvVEdlMqYhsAHgksAEAAAAAAIB964hsplRENgBEYAMAAAAAAABH0BHZTKmIbABOT2ADAAAAAAAAx9AR2UypiGwATk1gAwAAAAAAAMfREdlMqYhsAE5LYAMAAAAAAADH0hHZTKmIbABOSWADAAAAAAAAx9MR2UypiGwATkdgAwAAAAAAAMfUEdlMqYhsAE5FYAMAAAAAAADH1RHZTKmIbABOQ2ADAAAAAAAAx9YR2UypiGwATkFgAwAAAAAAAMfXEdlMqYhsAA5PYAMAAAAAAADn0BHZTKmIbAAOTWADAAAAAAAA59ER2UypiGwADktgAwAAAAAAAOfSEdlMqYhsAA5JYAMAAAAAAADn0xHZTKmIbAAOR2ADAAAAAAAA59QR2UypiGwADkVgAwAAAAAAAOfVEdlMqYhsAA5DYAMAAAAAAADn1hHZTKmIbAAOQWADAAAAAAAAdEQ2UyoiG4DdE9gAAAAAAAAAFx2RzZSKyAZg1wQ2AAAAAAAAwBcdkc2UisgGYLcENgAAAAAAAMDXOiKbKRWRDcAuCWwAAAAAAACAv+uIbKZURDYAuyOwAQAAAAAAAJ7SEdlMqYhsAHZFYAMAAAAAAAD8SEdkM6UisgHYDYENAAAAAAAAsNIR2UypiGwAdkFgAwAAAAAAAPxMR2QzpSKyAdg8gQ0AAAAAAADwHB2RzZSKyAZg0wQ2AAAAAAAAwHN1RDZTKiIbgM0S2AAAAAAAAAAv0RHZTKmIbAA2SWADAAAAAAAAvFRHZDOlIrIB2ByBDQAAAAAAAPAaHZHNlIrIBmBTBDYAAAAAAADAa3VENlMqIhuAzRDYAAAAAAAAAG/REdlMqYhsADZBYAMAAAAAAAC8VUdkM6UisgG4O4ENAAAAAAAAcA0dkc2UisgG4K4ENgAAAAAAAMC1dEQ2UyoiG4C7EdgAAAAAAAAA19QR2UypiGwA7kJgAwAAAAAAAFxbR2QzpSKyAbg5gQ0AAAAAAAAwoSOymVIR2QDclMAGAAAAAAAAmNIR2UypiGwAbkZgAwAAAAAAAEzqiGymVEQ2ADchsAEAAAAAAACmdUQ2UyoiG4BxAhsAAAAAAADgFjoimykVkQ3AKIENAAAAAAAAcCsdkc2UisgGYIzABgAAAAAAALiljshmSkVkAzBCYAMAAAAAAADcWkdkM6UisgG4OoENAAAAAAAAcA8dkc2UisgG4KoENgAAAAAAAMC9dEQ2UyoiG4CrEdgAAAAAAAAA99QR2UypiGwArkJgAwAAAAAAANxbR2QzpSKyAXgzgQ0AAAAAAACwBR2RzZSKyAbgTQQ2AAAAAAAAwFZ0RDZTKiIbgFcT2AAAAAAAAABb0hHZTKmIbABeRWADAAAAAAAAbE1HZDOlIrIBeDGBDQAAAAAAALBFHZHNlIrIBuBFBDYAAAAAAADAVnVENlMqIhuAZxPYAAAAAAAAAFvWEdlMqYhsAJ5FYAMAAAAAAABsXUdkM6UisgH4KYENAAAAAAAAsAcdkc2UisgGYElgAwAAAAAAAOxFR2QzpSKyAfghgQ0AAAAAAACwJx2RzZSKyAbgSQIbAAAAAAAAYG86IpspFZENwHcENgAAAAAAAMAedUQ2UyoiG4BvCGwAAAAAAACAveqIbKZURDYAfxLYAAAAAAAAAHvWEdlMqYhsAD4R2AAAAAAAAAB71xHZTKmIbAAENgAAAAAAAMAhdEQ2UyoiG+DkBDYAAAAAAADAUXRENlMqIhvgxAQ2AAAAAAAAwJF0RDZTKiIb4KQENgAAAAAAAMDRdEQ2UyoiG+CEBDYAAAAAAADAEXVENlMqIhvgZAQ2AAAAAAAAwFF1RDZTKiIb4EQENgAAAAAAAMCRdUQ2UyoiG+AkBDYAAAAAAADA0XVENlMqIhvgBAQ2AAAAAAAAwBl0RDZTKiIb4OAENgAAAAAAAMBZdEQ2UyoiG+DABDYAAAAAAADAmXRENlMqIhvgoAQ2AAAAAAAAwNl0RDZTKiIb4IAENgAAAAAAAMAZdUQ2UyoiG+BgBDYAAAAAAADAWXVENlMqIhvgQAQ2AAAAAAAAwJl1RDZTKiIb4CAENgAAAAAAAMDZdUQ2UyoiG+AABDYAAAAAAAAAIptJFZENsHMCGwAAAAAAAIDPOiKbKRWRDbBjAhsAAAAAAACAv3RENlMqIhtgpwQ2AAAAAAAAAN/qiGymVEQ2wA4JbAAAAAAAAAC+1xHZTKmIbICdEdgAAAAAAAAAPK0jsplSEdkAOyKwAQAAAAAAAPixjshmSkVkA+yEwAYAAAAAAABgrSOymVIR2QA7ILABAAAAAAAA+LmOyGZKRWQDbJzABvjaPx+XQwAAAAAAAL7XEdlMqYhsgA0T2AB/d1kK/20MAAAAAAAAT+qIbKZURDbARglsgKf8ZjEEAAAAAAD4oY53KVMqIhtggwQ2wGox/PXj+WAUAAAAAAAA3+mIbKZURDbAxghsgJXfP553EdkAAAAAAAA8pSOymVIR2QAbIrABfubh4/nvx18AAAAAAAC+1RHZTKmIbICNENgAz3H5gs3lSzZ/GAUAAAAAAMB3OiKbKRWRDbABAhvgub5ENm0UAAAAAAAA3+mIbKZURDbAnQlsgJe6LIb/MgYAAAAAAIDvdEQ2UyoiG+COBDbAa/zTcggAAAAAAPCkjvcoUyoiG+BOBDbAW5bDy5VRH4wCAAAAAADgGx2RzZSKyAa4A4EN8BZ/RGQDAAAAAADwlI7IZkpFZAPcmMAGeKuHj+e/H38BAAAAAAD4S0dkM6UisgFuSGADXMPlCzaXL9n8YRQAAAAAAADf6IhsplRENsCNCGyAa/kS2bRRAAAAAAAAfKMjsplSEdkANyCwAa7tshz+yxgAAAAAAAC+0RHZTKmIbIBhAhtgwj8tiAAAAAAAAN/peIcypSKyAQYJbIDJBfFyZdQHowAAAAAAAPhTR2QzpSKyAYYIbIBJf+RzZPPeKAAAAAAAAP7UEdlMqYhsgAECG2Daw8fzy+MvAAAAAAAAn3VENlMqIhvgygQ2wC1crom6fMnmd6MAAAAAAAD4U0dkM6UisgGuSGAD3Molsvn1cVEEAAAAAADgs47IZkpFZANcicAGuLXLgvibMQAAAAAAAPypI7KZUhHZAFcgsAHu4d+WRAAAAAAAgG90vD+ZUhHZAG8ksAHuuST+ks9XRwEAAAAAACCymVQR2QBvILAB7unh43n38bw3CgAAAAAAgE86IpspFZEN8EoCG+DeLpHNL4+/AAAAAAAAiGwmVUQ2wCsIbIAtuFwTdfmSze9GAQAAAAAA8ElHZDOlIrIBXkhgA2zFJbL59XFZBAAAAAAAQGQzqSKyAV5AYANszWVJ/M0YAAAAAAAAPumIbKZURDbAMwlsgC369+Oi+MEoAAAAAAAARDaDKiIb4BkENsCWF8V3EdkAAAAAAABcdEQ2UyoiG+AnBDbAlj3kc2TzYBQAAAAAAAAim0EVkQ2wILABtk5kAwAAAAAA8JeOyGZKRWQD/IDABtiDyzVR7x4XRgAAAAAAgLPriGymVEQ2wBMENsBefHhcFNsoAAAAAAAARDaDKiIb4G8ENsDe/MOyCAAAAAAA8EnHe5MpFZEN8BWBDbDnZfGDUQAAAAAAACfXEdlMqY/nf40BuBDYAHteFt9FZAMAAAAAANAR2Uz5HyMALgQ2wJ495HNk82AUAAAAAADAyXVENgBjBDbA3olsAAAAAAAAPuuIbABGCGyAI7hcE/XucWkEAAAAAAA4s47IBuDqBDbAUXx4XBbbKAAAAAAAgJPriGwArkpgAxzNPyyMAAAAAAAAIhuAaxLYAEddGH/N56/aAAAAAAAAnFVHZANwFQIb4Kh+/3jeRWQDAAAAAACcW0dk8xb/MQLgQmADHNnDx/PL4y8AAAAAAMBZdUQ2AG8isAGO7n0+f8lGZAMAAAAAAJxZR2QD8GoCG+AMLtdE/fK4OAIAAAAAAJxVR2QD8CoCG+BMLgvjv40BAAAAAAA4sY7IBuDFBDbA2fxmaQQAAAAAAE6u430JwIsIbICzLo2/5vPVUQAAAAAAAGfUEdkAPJvABjir3z+edxHZAAAAAAAA59UR2QA8i8AGOLOHj+cXYwAAAAAAAE6sI7JZeTAC4EJgA5zdeyMAAAAAAABOriOy+RG3IQCfCGwAAAAAAAAA6IhsAH5IYAMAAAAAAADARUdkA/AkgQ0AAAAAAAAAX3RENgDfEdgAAAAAAAAA8LWOyAbgGwIbAAAAAAAAAP6uI7IB+JPABgAAAAAAAICndEQ2AJ8IbAAAAAAAAAD4kc65I5v3/gLAhcAGAAAAAAAAgJXOeSOb9x4/cCGwAQAAAAAAAOBnOq6LAk5MYAMAAAAAAADAc3RENsBJCWwAAAAAAAAAeK6OyAY4IYENAAAAAAAAAC/REdkAJyOwAQAAAAAAAOClOiIb4EQENgAAAAAAAAC8RkdkA5yEwAYAAAAAAACA1+ocN7L54PECXwhsAAAAAAAAAHiLzjEjmwePFvhCYAMAAAAAAADAW3VcFwUcmMAGAAAAAAAAgGvoiGyAgxLYAAAAAAAAAHAtHZENcEACGwAAAAAAAACuqSOyAQ5GYAMAAAAAAADAtXVENsCBCGwAAAAAAAAAmNAR2QAHIbABAAAAAAAAYEpHZAMcgMAGAAAAAAAAgEmdfUY2//HogC8ENgAAAAAAAABM6/iSDbBjAhsAAAAAAAAAbqEjsgF2SmADAAAAAAAAwK10RDbADglsAAAAAAAAALiljsgG2BmBDQAAAAAAAAC31hHZADsisAEAAAAAAADgHjoiG2AnBDYAAAAAAAAA3EtHZAPsgMAGAAAAAAAAgHvqbDOyefBogC8ENgAAAAAAAADcW2d7kc0HjwX4QmADAAAAAAAAwBZ0XBcFbJTABgAAAAAAAICt6IhsgA0S2AAAAAAAAACwJR2RDbAxAhsAAAAAAAAAtqYjsgE2RGADAAAAAAAAwBZ1RDbARghsAAAAAAAAANiqjsgG2ACBDQAAAAAAAABb1rlPZPPB6IEvBDYAAAAAAAAAbF3n9pHNg7EDXwhsAAAAAAAAANiDjuuigDsR2AAAAAAAAACwFx2RDXAHAhsAAAAAAAAA9qQjsgFuTGADAAAAAAAAwN50RDbADQlsAAAAAAAAANijjsgGuBGBDQAAAAAAAAB71RHZADcgsAEAAAAAAABgzzoiG2CYwAYAAAAAAACAvetcN7L5w0iBrwlsAAAAAAAAADiCji/ZAEMENgAAAAAAAAAcRUdkAwwQ2AAAAAAAAABwJB2RDXBlAhsAAAAAAAAAjqYjsgGuSGADAAAAAAAAwBF1RDbAlQhsAAAAAAAAADiqjsgGuAKBDQAAAAAAAABH1hHZAG8ksAEAAAAAAADg6Dovi2wejAz4msAGAAAAAAAAgDPoPD+y+T/jAr4msAEAAAAAAADgLDquiwJeQWADAAAAAAAAwJl0RDbACwlsAAAAAAAAADibjsgGeAGBDQAAAAAAAABn1BHZAM8ksAEAAAAAAADgrDoiG+AZBDYAAAAAAAAAnFlHZAP8hMAGAAAAAAAAgLPrfBvZvDcS4GsCGwAAAAAAAAD4NrJ5bxzA1/7LCAAAAAAAAADgkzYC4CkCGwAAAAAAAAD4SxsB8HeuiAIAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAD/z94d1caNhmEY9UWJFEIgBEIgDIQwaBkEgpdBIHQZDIRACIT9LY+0lXb1tE1mkrHnHOmT79/rR/4BAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAA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AAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgC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NgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAARtX8nE+AUck4AAAAAElFTkSuQmCC" + }, + "d7a423ad-3e19-4492-9200-78137dccc136": { + "name": "VivoKey Apex FIDO2", + "icon_light": "data:image/png;base64,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cAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo2OWExYmMwNS00M2JkLTRhMjQtOTQ3MC01NGM4YTI3YzcxYmMiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDJmZGJlZmYtMTJlOS00Mzk4LThkMDQtMDU0MzExYWZlYjE2IgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZGNjNjkyYzctYjJiNS00NWFlLWFmOGQtZjAyZWUwYTI5ZDU1IgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iV2luZG93cyIKICAgR0lNUDpUaW1lU3RhbXA9IjE2NjAxNTI5MDEwMzU3ODAiCiAgIEdJTVA6VmVyc2lvbj0iMi4xMC4zMCIKICAgdGlmZjpPcmllbnRhdGlvbj0iMSIKICAgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuMTAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDphYjljYTRkNC0xMDQ3LTRjZGQtODAyNi00OTI1YjY5ODNjYmMiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OndoZW49IjIwMjItMDgtMTBUMTA6MzU6MDEiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+6HMtNwAAAAZiS0dEAP8AAABBMvwN9QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+YIChEjAPBJR7wAAAkDSURBVFjDrZZ7bFP3Fcc/92HHdpz4FcdxDEnIC5KQ8AyQUJJQCpQWNlhbtI2qa9Vu09ROk/bf1D/2R6f9W01bJ23a1kntimgR7WhXSimlkJIGSElDAiHk6RDHeThx7Nj4fe/+IJiYAK2mff+6uufc3/mec77n3J+gqqoKoKqQUhSSKYVUSiGRSuIL3MLrD+H1h5gORvCHIgTCMW7FEiiKSlmBmcfWljI84efSoBedRsZs1JFvyqay0EaZ04qIgiiKiJKILIrI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", + "icon_dark": "data:image/png;base64,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cAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo2OWExYmMwNS00M2JkLTRhMjQtOTQ3MC01NGM4YTI3YzcxYmMiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDJmZGJlZmYtMTJlOS00Mzk4LThkMDQtMDU0MzExYWZlYjE2IgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZGNjNjkyYzctYjJiNS00NWFlLWFmOGQtZjAyZWUwYTI5ZDU1IgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iV2luZG93cyIKICAgR0lNUDpUaW1lU3RhbXA9IjE2NjAxNTI5MDEwMzU3ODAiCiAgIEdJTVA6VmVyc2lvbj0iMi4xMC4zMCIKICAgdGlmZjpPcmllbnRhdGlvbj0iMSIKICAgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuMTAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDphYjljYTRkNC0xMDQ3LTRjZGQtODAyNi00OTI1YjY5ODNjYmMiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OndoZW49IjIwMjItMDgtMTBUMTA6MzU6MDEiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+6HMtNwAAAAZiS0dEAP8AAABBMvwN9QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+YIChEjAPBJR7wAAAkDSURBVFjDrZZ7bFP3Fcc/92HHdpz4FcdxDEnIC5KQ8AyQUJJQCpQWNlhbtI2qa9Vu09ROk/bf1D/2R6f9W01bJ23a1kntimgR7WhXSimlkJIGSElDAiHk6RDHeThx7Nj4fe/+IJiYAK2mff+6uufc3/mec77n3J+gqqoKoKqQUhSSKYVUSiGRSuIL3MLrD+H1h5gORvCHIgTCMW7FEiiKSlmBmcfWljI84efSoBedRsZs1JFvyqay0EaZ04qIgiiKiJKILIrI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" + }, + "ba76a271-6eb6-4171-874d-b6428dbe3437": { + "name": "ATKey.ProS", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "ee882879-721c-4913-9775-3dfcce97072a": { + "name": "YubiKey 5 Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "8876631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "Solo Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "fec067a1-f1d0-4c5e-b4c0-cc3237475461": { + "name": "KX701 SmartToken FIDO", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "b267239b-954f-4041-a01b-ee4f33c145b6": { + "name": "authenton1 - CTAP2.1", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbAAAAGxCAYAAAADEuOPAAAACXBIWXMAABcSAAAXEgFnn9JSAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDMgNzkuMTY0NTI3LCAyMDIwLzEwLzE1LTE3OjQ4OjMyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjIuMSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTExLTIwVDE0OjQwOjUwKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2NGRiZjU4ZC05OTY4LTg4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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbAAAAGxCAYAAAADEuOPAAAACXBIWXMAABcSAAAXEgFnn9JSAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDMgNzkuMTY0NTI3LCAyMDIwLzEwLzE1LTE3OjQ4OjMyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjIuMSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTExLTIwVDE0OjQwOjUwKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2NGRiZjU4ZC05OTY4LTg4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" + }, + "b50d5e0a-7f81-4959-9b12-f45407407503": { + "name": "IDPrime 3940 FIDO", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQwAAAAgCAYAAADnlUZqAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAK1ElEQVR4Xu1dDXAcZRm+NOAfKog6WO0QcreX3O71R41oHdSqqDAOg3+cYEXBolXRTEn220taKTc64mgBqzBiEUVpBdqiwwhqSdIS2upYSgvRtpTSckljWzHagjpSRdr4vLtvjrvk27vdvd1Ljn7PzDN3t/d+7/t+f8/+78aK0NDaar2qOdXZoqWyH9R0a0Fct67WdHGTZojVCcPqSejW1oQuHsOy/eBTsDmM/54ZT9j+LWGIg7DfB/sBcDPsf4XfP8X3b2uG1ZHQzU8mUuKdyWTHm5qaci/jHAKByif0bBr+LwaXIPYPkMdqfL8XdWpls1AA31/QjOw98L8S9b8BXIR2+nDc6Dozlsk0slnkQMxkPGXO9EJtVnYGF4sUyVnd8UTaep8bw+6LakBj5izdbNJS1rxEWnyWxg36EmPdWoPPDejf7eATGMsHaDzTuC6hbj0N/pXmAsrugs0WLP8NuBJjZJmWElcl09mPJ1JmW0tL5+uiHBuGkXsljX87ni4EzVnk9AvksQn57ESdhrB8BMuPjOWP//4OHsR/e7D8YdTlftRhFfgdLG9Hu1wAfzr55jAOkiQKhvVbGB6C0//i+2iNeRx8FgnvRfxfainzSk7NE0iIUPbf43wWmNTNd7BpKEA7LZfFAY9zp3yZTSMDiQVi/U+Sg5QYAIfOmG2ewsUjA/rhW7L4Bermj9h0UoB2OB+TZTW4B/k8OyG/yCiOoW1IYH6H8XPz9LbcKzilQGhpMZvhZyHGwG3g42Bk85Z8o90G8X0NiSs1Iv2QGk8KdWszt4snIP8RqR9mDQXDIdZSbBoZ0Il3S2OXZXYpF48MU14wnK1beW41pL3FEQCJlPVWtDG2fuyVrNR3tBTdSjB8YrIFoyVtno2OCzBgxDNBB6pXKMHwxiD9gK3Kc6PckvBGJRi+McmC0YD4fdK4Xoh9W/YTCZRgeKNvwchkGtG2e2W+akslGL4xmYJBaxlpTI+kNRQdmGR3oUMJhjf6FQw6cCrzU3tCMLDWuQsd3R+Aw3KnBQ5KynjhjdxOnnDiCEZuGjrsYWlMJtpiWUK3BmT/FfEudhg6UPe6Fgz0bR6fa6MmnY3klDwhaYjLUU6es27t0gzzm7VgUu96D6fkHxCa62UVGCMq8g02jRQnimBoRvYiaTwm2ntfW9vCk7W0dYHs/wJ163k6eMZuQ0W9CwbG9K1sOqWAvIU0X5tiDZtNbSjBcGEEgtHWdsvJ8E2nAuUxibp5hWM92oDf2yb8X0Kx3rENF0owogHm0hJpvjaVYPjCiSAYibT1eWksJibCk/Pm5U5ic8rxQpldMRPp7HlsHhqUYEQDJRgh4sUuGHSRD+pIV+TJ4xH1LG9djCHTiMlR4ViG2E7HRbhAKFCCEQ2UYISIF7tgoJ2z0jhMtHOejl2weQFY/lGZfSnFfDYPBUowokHCMBdL87WpBMMXKgqGIS5vTptnh0XU+05ZnAJDFAzD6Dgd/p6WxmHGDfFFNh+H0Qb0waOyMmOE+OUNI/cSLlA16l0w0F6747q4pRpGcdqa7kuR5UtEH45gDmwKi/DZj8/7IES34rOzeaaYzWlUh3oRjJozRMGoOAENa0i2dTGGeEp8TFJmPDvYvGrUu2CEQbqhksOFBsyli2WxasTj6Nd12psXv57TCQYlGC4MSTBaW603oo1db6qzqVtfYnM56ApBw9oxoVwRMYlGNK391VyiKijBiEYwmlPdLbJYtSTa7qHiA+u+oQTDhSEJBtpvhdT/GHWxv9zWxRi0tPiEtHwJxbVsXhWUYEQjGHRwGuOh0gV5kTOeMi/hhPxDCYYLQxCMs1qtVgzu8revpyyPjwHwspVh/SuVWjKdCwSGEoyoBAO5p833op+ek8WsFdF+wa8SVoLhwhAEA37WTPBbRHTcAexGvJTNHfQMNcf6Bs+P9ebnxfqePJWX2kCZzHgfExjCGQIlGNEJBsF+EJEudsvi1obiT5yKf9SNYOjWZjTyfaHRud9AHotYpWA4NxqJY1LfTNT5K2wei60fMiAUD4KjBfbmj8b68stj2w7aD2qhfU/0xy6ZrzHS2qulpTNl+wyIuhcMjBU661QNm2cuPoPDRYTRBjpbR2MAOV9HZzOQ98/w/fYwiPHtfje0bv2Fk/CPehGMOrsOo/Lt67o1XDgVuiE/BwLxjxKxKOXG2M6dti36w8ORdnGP7TcgkFudC8bUvA6jlkikO8+Ttg2IMXSYzfxDCYYLqxAML7evo77ttnF//0nYktghEYlxHLqazJ2tjEqbs9iySWXn2v4DQAlG/aOsYBjWATbzDyUYLgwsGLlpKLtV6pNJHVZ4YHLf/nfJBWICh2HdQEXi6ewlMr8ldJ5HYtv7hRKM+kc5wUD77GUz/1CC4cKAguHp9GdKXMXmEIx8u0QcXPjYa+0ymUwj2utxqe8ioo4X2vY+oQSj/lFhl+SPbOYfSjBcGEAw6HoK7A6Uncio58GmpsteeB1D79BX5eIg4f3Dp3OpGOLMl/kfxx2xzFrfj8VXglH/qLBLsoXN/EMJhgsDCEYiVf72dWbpJdw9+86RisN49g7uh3VhF4PF6QmJ/1Lq1gIu4hmVBAMT9u7x70wJg/TYfU6hLJRgVEaFXZIH2Mw/lGC40KdgzJ5tngKfB6S+mPj/0IwZHS/nIg5GRxshBgNSkSjlYi5RAPruUlmcYmJy/XnG3HExK6DiFkZExBjYyCmURSXBQDuPoA5bo2bSyL6dU/IE3iqUngYNm2gD17N0+G8Vp+QfSjBc6FMw4rplSf0UETFNNi9Fz/DMWG/+iEQkHPbmN8S2bZt4+bhzj0n5J3iBdFs1l/AE1L2uBaNWTOriA5ySJyDv78r81Jyery6WQAmGC30IRtOc3Glop8NSP2PUxVNl1/Tr8q2xvvx68Pkisfgnfl8f6x90fQUl4n5GGq+Yujhy5qzu13CRilCC4Y11KRj0WkgtF/wmRSUYLvQhGF4mGAaLYPPy2Dg0PdYz9H7spsyN9QxUfC0iXfyFPtoni1lMGqxcpCKUYHhj3QkGxCKpW+/mdIJBCYYLPQoGvYQa9uXf71lp66JKlHt8/QsUR+0XTXuAEgxvrA/BoLfr2QfHr/GzlemKKSMYunkHTSzElL4+sFaCgfo+B+7WjOzn2LQsnNcGiD1UTubPodnF5pGAzpggvutWBur6H7tOuriUi5QFXSWKMt/HBN5EayXUr+w9McEpjvGK4vfIbwVdw8IplAWNBZS5DvWhN5Xn4edoqd8oiFyx2wk+iu/0Iuil9KwTTskT4mlxDtrzRm5XjPUo2pXe6G49gjxvw+fChNGhcfhwQC9jaTLEG9xoGFeWviY+UuSm2Q+coXdy6NYiNOwyVPrHGBh3JozuUCseT5mXQfF/jhg/xOfXNd28gjo0aH3pLAlNNGdtL5Yi55vQgbej4+6g/9gsMqAOH3HaSfwEbXcDvmeThvUpTe96y4QzM76Qm9Y0Z9FpdPcm6vNpsAt9stxpO+vX4EbE20oTCcsGSonl+B/f6Wa/VcV50aSPx7tODeEBxg10xy+dkoXgfAgxFiDe19AO30M/rEQO9yLmA4i/Bb+3l+bnkPIHN4PrUL+1+FwB22vhox1if1G81XpbvA25ZjK+r2lxR24a1d8RPzEfuwoWcsEWiJMzYj+I3w+VtKshHgH/APZSnqjTzfi8xh67unUuPdrA28NxYrH/Az3tI4j5+TOLAAAAAElFTkSuQmCC" + }, + "8c97a730-3f7b-41a6-87d6-1e9b62bda6f0": { + "name": "FT-JCOS FIDO Fingerprint Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "a1f52be5-dfab-4364-b51c-2bd496b14a56": { + "name": "OCTATCO EzFinger2 FIDO2 AUTHENTICATOR", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "3e078ffd-4c54-4586-8baa-a77da113aec5": { + "name": "Hideez Key 3 FIDO2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6gQEP4ALiBCCehksBEw7x/1CsDdW8D0kMBbBg0QgCAkD8EUncCUo/RlLDiG4AigQOIIuk9i8QM6O7AJ9mdHX/kcPgPwmaUQxhItFmdHAFZAA3EJ8hEBv/ccjrgAyIB2JjMl0ADoNpDBQAFiICiqALYGAdiZb/R3YBI56AwutC9LxwgATbPdHDAOYKJSC+h0dzABC7APFebIHIiJYvCAYsQAAxEigPwoH4CxBvJSUa/xNwESO+AgU5SzOiacLqPSY0zVYEEg+GISxkZGdGpAwGTwfpZJQFcBf8J7M8AOn5x0QgtcGwE7FJGRfYS2q9AAL9BLL1TPRCFR0UYUkPyCANiE8wUVCggoAlshfqSC1MkL0AckUjOWmBCVttQ4TtjLhiASSxBy0NIGMt9DADCCBC5QE6+AzEPGhi36DtCGSwHIijiK1XGIhMzf+hljOiYW40ficQR6LpSya3gYMc5oxEJrkKLOrn4KqimfBYDDOAiYEygO5wkPmquApUEBClMHMR45BbQLwduUB+DcTngdiIgfYAuVZghYWACBB3k9G0QMaTyXDML5ADQqGcZeQURUggh5zmDRM0Hw8YYEJrdFSREI/mBFI7SYX5QijdSoLjT5FYPsCACbYqOYFA/FITnIbS5thqo1QaOwK5kDuFrSScQ2QLl1QgBzWvHz26WAgUFtJA/ASL/B1otj0G7dNKQhv8oKhkJaI4JrqT9BRNIyjE/gCxCp4mzFm0hIYXAAQQqe0BlAYV1KLvQLwfiO/SopuIDHyAeDMJ5ct/YhUSAieghm3GEa/Y4vcfUhOMohD4jyVNyBDb9wGCq4Q63LhCoAGL5Yx4LCeU4v+T4oAlQFxPZhmP7pALhByB7gAzII4mYwQJFzDE0erC6YCTVLScAUf3F28nm9qW4xqgmIovDdDCcnSzs9Ad8J8OlqM7oh5bdUwvwAfN6mAHaA9AU/Azckl4gILUTWnaYWKC9gkotZzcBkwfOf2+51SIgjJYDYvsAC4iNUvgkfMi0owmmJ3IDphHpOYleOS2EWkGO6x2RXZAOJGaY6mYG+YzQdtwlBSrDNDGKTm5YBoLtF33nwqOIBbsw1cbfqFDIeSIzwHcdCwN5ZAdgBycLTS0FDmqH6OHwCcoXU2nyggjCvixNRho5PvPuNIARoOBxi0jvC2iDzTqlhPVL2CERkkZhRYzA/FGfOUGC4GgArm8E4vcGiDexAAZcAR1x02hRbk5joKHkdyuGa7BihAopri0ZCIh4YBwDxFqrUnpTQEEECXjA8QCDSAuhPa4SClpQZPjoNHXRbR0HBOVzdvOgDmEfJ0BMsWF7vkSpJjiBeKXaPKgSnohA/aZH6PBEgAFaA7zwKHuI9STyOMpvWiNAAk0+Vl47D2LZOcvegeAHpLl/TjUvEPzjAAZLZ10NDNW4FDHiuSeB7QMgMVQSy4S4WBhGmTXSCTzFXCokWfAv3iGrACogxoYg61FTWSSpTZ4iGSvH57an2BAkDpECQO8dGq8EwM2M+CfXPgPTb1xpKSAYhyGwUJ9sHgel/uwdWT/E5sCdjNAViqhB9R/hqEDcKWI/4Ra4+vRPG/BQP5Cs8GaInCOEAcyQNapgcBMqMaTDMMDYFs6gREA65AUZzAMTwDy22wouxs5AJC74Ep0cIgntLGE3IpcQadASEVqisMDAHkIgJbDATDPgsYwBdHkwpHk99ApMDxAAWCJpQqkNggjsSB1plHBq4/eIWNiIGFunQKwktwYorI70McTNEEB8B2LwsBBUmjdorJ5LthagvuwKFxFo4YJqWML96joBlMsYnuYcFgCaiFy0iAQDpCg1ovK9h/FItaNbd0WDLylQZJ2ROvju0F7c0oM5C1CI6Xww7aY6Qr6yjlkAEoBwTTO47uhvbn7NLbnAo7IQGkJYusYrRkGrb9XWMQuw7IjcgCAtlxZkTAmMBQAqHMnikVcD1dv8DgD9tmFoRgIU5E6dzhrJGwDIqdwFERDKRDmYmnSb8LmL0JzU9dArSV8AwqDEOwCYldi2yGEBkW1cAwoMA1Szz9G83wdoQgjdW4OucDUHWSeB0WMDJrHmwlpYiHRElgggPrul7DIf4PmtQ0MkK0B1Bw8BQ3P+UILNi1qNbmpMTk6g4H0fYXUBKB1T2RPj1EjL2egNWNraOhZUItRGM0+iuYGWWjgyFYG7JtRWKBtf2doQ0QBqcPFDC3AbkHbIqCS/DY9kg9AAPKuLSSLIAofNaRAJBISI7sQWkSQJUZJmd3wJaxeIogsEIwuhD0I0oNG0UNlRQ9ZUYEQBRKIkRHdyCLyISqQIgsiqMgKoYcSpFDr9J/h36Yzu7P7z6y7fx/8oLOzO3O+ncuZM2fOhuEfIKOYfgW0QEHhPxEBWJmhMCszLoQyammMKPNxDw6el37/jhi2CVgZA2TgG22HpIHzvIvwqlNsOUTaG3rGd+o+kSZgMVUWz/hs9MiL50DQXU6chm3wyI/5btLzO6NGwHyqWI9GXrGTiwrLN0d6C6Wv0HjGOirvXhQIGFEYG2Q0g/tevkA35SskbdMNlURE3VgQsEdzYbSN8hzw+fwPNEDnaKxCz6ayUg0yC+CUle+RZzeY8XgdpJeEU+ZHjbUAuuS9stkCRj2Ev0hv3LS7bz8912ujpA9oz88GAW7N7AdVsMayTnGTynnkkucorU+MEuAm/FZIHsQIC+gOO83lOuoQrabGAO24PWNg/MggvSOLub6DFKljqbSAURdVNSqmsXG0eOLQ4mW4cSPgiiL9KSTc5KKEKlDHt+kNQkAJ8P7w6P1fCtHEflBHtBnyS8AzJg1D5qyHaAPruFZhNdquS8BFJq0LNOMFRQDXqUvIOKNLgOwT/AASxsg4AQdFbnu9w4sA2Vni3e/fcognbjCK2QYvAuTl6HSIN7A7N0ppbSoCjkRIyTEJPHZ2WtJcWQIa0lB4gZ20jhBYIxOQ67iYBekJXEkKU/s5mQBxOhFPfYxA+qJYHtsEAcI5ugz+H8zkZoEFIRXeAX87SmOMvZUhtgCxWvxDQG6IrLeRwPJ8jPE87oJ9L5Rljr83iaVkVUjCo6Niuab9wdYs5HQMLxQtIIymV60pvJcdIlXIDmDZmUy/L7ZQ8NUA96y2UI950v9zMiEZnl2gwnChQe2FrSG0zGlIwESP9YAJBSQIikIgYEImo/isMlxIHkQDXFy8DBGx0Yl8wwUH9cAYNlwPzqbx51sIA5aZfxrwPtOHsbl4Uf1IwAvmwgzDhfcEuMf06TXOsNOHBHAfsqg1XHi5z/wHQxoXBpCA28yFOguF6e5Eo87QZLjsQtUFJIA7HzzZAgHD8G/QTxnoPmfD9N7IpN3xeitIwhcLlRGaJ54TwrCOQ4pWaBLceHLKuRzmBsIWy5VC97drIQivQqeTAK6JbIH0QL3bRUFAl+J6fhoQcMJtnZEpNUkZ12MufI4ifRdHALepWBpzArhQo0NcF0C8VDzkeIwJWOZlFPHaGkPsjanwZxXpvW4EdCtuao4hAZw2O1c1CzgxhUnbnwZv/xPXzTkC+hXKyaGYv/0CNz1ABuebvy8mwnPOXZu9FCEO2UxaewwIkJ27MPzf5SAE/ITkh5EENkZceM65q0RHFVYB4wfIn6V6HVHhxzPCGglri9GFnZ5jRZbsBaniq1/hdQlA1EjL488RE34htQBfwvshAIEuNOsc/+MWdzWM7UnyImqhTxzjlq+NVb+VdwYhwC1utN+hqUvs8+Mg1OQ18ATAJLJPIOk/HOXheCS8Wy4oZi5XBD04iSQ8hITfvjzi4k92XMbzgWh9fk7a2HtHN8KdqTxSVGZBwkyGz/DjoodxQgLtb6RycnQpJD7PMaiRF/NVgPmN15PgYfEx3QWAebPYGhaF3Pe7qNz6VB9kagB7TBXCpvjOouDiM6fGfJdNj+AD1HexkpWgjkKtC/GBAfHp4cOmGbV5evy+NBvMpkXWEpq+pkJyBxi70lsiDI/E3gLzu8MsfgnQ3rmGWlFFcXx56FJkJISamMZNL5mifbCIougq9pKEypIwA82ulN0MNAsq+xJhoWCZ5aOXVpbaA7OXkd6MoqL8EJRmD5MkP5Qa2APLMszfPWt3htOZmT2PM2fm3P2Hg9dzZvbM3mvN7L3WXuu/GsEfUG+QzkMCZZt+BquPo69+TtBFU4tUYiNKOr3+oS91NHmv+hCg8f5OPzssX/qFwTEFvGdYN4h1nqBPVFoR/czUJlqoLcJ5KEaXrgk3S0JKk6xRyvn9taoxvt+z+D2ogz0jgfAPSXlvqL8uspfod3HA2hUH3JvahrlP3iDzxa5ip1MABQuHTz2DyLw4V5KHmWEqTpQK8RBTAHtj+9SJcJt+Z36nlMWXCa/JivAuNXpMf96TnIXjN1oBmJNf9gzQlhQG6C99uk/1CBTi6PUR2lirFqk5n7/ToBlur1JweFz79DQFYDX8hVRyJJKS1vKqnSXlNCeEdaw+3T+keM+8Da71KARP96Py//jSqMDLeEDHYqsE0yEUWgFwUr2uHYXhY2SCtti0m+4RxskqjCzTvPar0rV4FGJZwjbPVovjiL5tejWDAlyvHToktUNPbICL9161WHqpSbcyZ2sXFOIWj1Ky//5+gvYmSaWQ/VVFVADD6vRczPNxTozSweTtcX9WjpGUsEPne6MQSQJLTGrhoiIogClEFyfGeqPa4QwYUbTbmsjfcp9HGeJWLpqtY7s6jwqwTPwL8QUB1+dgqdSR+EWaHyukdq1NW0zRsV6YBwWYqjdzc4zzGAB85Xuk58JUmyVf4NsY5zL21zRCASA2JaB6VYRzWOEO0g4/Kw5e4PA6XcfmqYjnEgm3XWK69eMoAF4zCOROszy+S230Vikz6DoEo0MVIUqm4Ai1lqbXWwFIeVxseewG7chF0txULPXCMoleY4u3x6Z6KABPL5sw51oca+iir3QyTAUbxY5C14AHjvKd/dJSgHado8Kqzb0jdnTZDvFgKIRtwoEoX4qL/KykCnC5hJcE/FyV41Ino0xgAuJsPISEYo6NqwBjxD9/FPwq5Y0dqgn86eSSOV5VRegMOQ5O0NFRFYCk/aByDczvbGN+4+TQcCxVRXgg4Bh2GttsFYAdrtd8GjIFyza4cc8d7lbZrPWR8xu2CoApUR1q9ZZYVqpzaDgmq6y2Vn0/TGpQsVUrAAsLL0kGQRUDdDHoUCyQrXGKlOMnDCAMvThIAarnESJhfnJjWVhQg6h6V3W+9z9e/3GHvia8YFuWOPrfm2hQWOPgOh2q9jIbKjhOdqnCH26ivhJMW82XSuQRYXivVCtALXOCsGkCIj8p8CBAjvu4CjwKiFtkl/OjAvedoJpa9NCdRgHMFEC6kl9SaxHrSJDkYaJvu2II3wzeh1IJ5y4it/75Pt+PVVP/PwUI8uJdULBO87STvpVm/H27Tg0LCzYW40L61K0AJCoG+Yz57biCdBjTZ0Yd258r4a7xvKCfzvdBVkJ/FIBEyuEBBw4MaSgvWJfRfbZL9KCNRoCd26C6d8h8mClZ2jeksfE57yyv+yxZjKbFXFdkiTAafOQ+oKSWQNgCZ0LOOzsq4+uVapjMeUOY8647MLWkwg/bFj5T8s0f+nMDrvl3jscDqtCwUijd+YkIHhKEAxaNXp3jDrPRkWV0Mbugm3I8HjbTIRFeB1EA/P02xDaTctxhsoZmZni9jhyPRYvlw0qU124UgIiezyxOaMv5WoC3wGUZXIdSGB/keBymiA87bBXYI+iuH8KroMuy8ZtyvvAxcXPv1qHt9dr2xzkfg07L4wg2PVzyDNw+i5MmSPpVtuqBcSqsh1Noy+T1TSxAvydZ+kKY8jeLZ/XPbt9ay4vcI8XBbKnk4eEXh5Fjd8i8SO7eOZJOZm/WsC089IJaAeKlicMjuMOyAQpxrhOHPAE63wUWx5GkgxPre6my/2HueMzyYrxaj3djnhu0Hv08aHnsAiP8agUAsFrZVM0iTOxpN+65wWqxS/Jhipvn/aL6pN/EvoIgpEmz3Ng3HIvFf9+/lv/inyAFMPa0bZWUR6R2kRGHbHCDlLO1bTCvlnlcCjh4TQTbe5iTReYYE2EaXuH3UAfNG9epcG0AE+dAJ5PMQLDuFstjIZnyZXAJWzjgWrUpo9hblaCPk03dQZCubX1u+AYD9wVsVo54/56wtAzYJTvRyaiu5p6t8B+S2gXUIysAgPbNxsdMGDmetpOcrFLHGWrG2ZQGmnb0M8em0SgUMeSVEWQQRqsO1x8ZKYOczFIDKfg2Xlpo9uAbfsa24agcQVCZESEcxvIFYTNxBiOc7BKDsHybsi4r9OGLRJIdlyZuqmplGH3rdjVXHOIBHoaw2AOcd0MlJgNpEqJIAkkIKL0j5DjMlclOlpFB7EVYjYOZuujeFfciaVDFUlWTbdOgjSS2H+90MrUGMQjLA35fpGO+POmF0iSLvlVvaqnP79R8W+JkG4onpUyPHyT429O6WD3o4jv1Juf4KMl6J2NfQL1zo890kKrgDbKoG0ju4UYJzqTZowvGbfrh76+lzETWDMAvMlytIj4j9d+BIQvoS9SkrhuyLhxJjZxVkqwcCpm/O6Vcr2+nLoB2q/mzR+pPOY+zC4p76FfgSyZaeoj+PURN4Lig4BWU+y9lJZBGVg5FGeDD7emRRbzlyGh+sREXb2TZOJxJvfVtwHby2z1I6NDwtWrf+zRK+I1WAC/YRBovlUhc5svnRSNXCw6cZSt1LWT6d4UERyf3OAWoxlc6F5Y8g3ahlN2de3Ms7L06rZ3nuW+cZdN1vZI7NEP1cLahiYmDEGG0rrD711HAWCkwkcBBBIHUj0UevF5HjjTDW9YhLv4FMFbB7o//JIUAAAAASUVORK5CYII" + }, + "ec31b4cc-2acc-4b8e-9c01-bade00ccbe26": { + "name": "KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d41f5a69-b817-4144-a13c-9ebd6d9254d6": { + "name": "ATKey.Card CTAP2.0", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAU0SURBVGhD7Vpbc9NGFD62bMWOLzi2kwJ2LpAWSgt0IEBvT33tdKYz7Vt/YB86w2/gpZ02hYdOAk+FaSBpIDeH2CW+yz3fareR09iyoks8jr+ZM9autOv99O3Zc7RS6KeVtQ6dI4Tl77nBmPCoY0x41DEmPOoYKA7Xmm0yaLjDdZhCFItqstQbtoRB9vubc6RHwtQZUs6hEFGjZdDDp69sSdtOaSiraxpFwmGKasNpGJvOv4PMwoF8uDOs0low6Bhtp/Rhs0U/3L5CUZ7SwPPdCm2/q5KGeXSGaDPBmUSc3s+nRLnZatOPK2s0GY2Ici84Jvzryx36c6/C0+hsCbeMDn2QS9Hn89OiPChhx2EpzMqC7Em+FKRhDBiLUzgm7BYGT8U2qwPDcdAIlDBIxiIapSeiwnCMuiARGGGsom3DoG8/mqWvPywIwzHqgowCgRFuspK3Lk7J0hFQh3NBIRDCULDFSt6+9H/CqMO5oFQOhDAU/HgmI0tE7xotqnK4U8A5hJkg4DthKGewgveKOVlDtPJ6n/7Y3JcloqVCNjCVfScM5a7l07JkhqXnpbIwRTDE8fT6dDoQlX0lrHx3yaLuKqsbDoWFrbw5Uvnu5VwgKvtKGDF2kdM/PM0orG69pSgyNbbVN29lLYnsCdf6HZd9Iwyl6u02PSjmZQ3Rs619fkw3p7AwWadwv5ATbfxU2TfCeJpZyCSFcgpP/i6RxmobrCIMx082SvIskc6ZF9qgrV/whTAUarQN+mzOfJIBXuyVKaVHKMmWkIbj1ESEz1XkVUQPZnOirV8q+0IYCs2mJ7u2WxZzafru5jx9c6PYZaiD7ypM6lEqclu/VPacMJRpskLWldkpltiX0YcfKntOGItsgRW6ENNljXNk4rrow48F2/GOx/KrXXpRqnQtRlYgrOC53BSn0xWS6qzaV1feo8sXJkV58+CQHv21RROWvhCLeVj/9aH12FnBDFjMpujTOTMK+Lbj0Q/IouLst1enkrQwlRAZFkjCH4UJyaz3V24GyPO4Fm3QFn2gL683CTwjDH+r8V3+cn6a7s/mxQo9l0mIemzFmIYrrYqZdeo8rkUbtEUfX/Av+vTSlz0jDPGy7Hv5REzWEP28tt1z6p+EKE//X17uyBLRdDIm+vTSlz0hjPE0OENCPqyw/U+VyvVWl552gN8e1BrctiZriO5cyrK/ssqy7BbeEOYpl+L4WZCLEbC8vifeBiCFHBS4Fm85Hm/syhqiIk/xJPft1bT2hDDe69zlZ1qF0mGdStW69FlnQJtdtGdTuMN9I/vyAq4JYxXVtRDN86qq8Nv6DocazZG6CmiDtsvrRyovcN/i3ZEHKrsmjDuPFVWhLHyw3jN+DgK03WI/Rl8K9zxS2RVh3HGocZUTAAUoE5NJihtMcB+/b+zJkpmLI0Fxq7KrkSHb+cSyE4nNudeVqoipboGXdZvlQ9Gnwq2LGfGfbnBqwlg1xS5FNkl1Tg7wfLvMvou6fr5rjcv9YjT6wPnHFl++MZMRbyvcqOwqlwbpGq/QZiQ2CVhz5+PAQOM84Igk2mK1qnyzes0I9I82aX4QwTGuwxcJTc63seEXeC4NFZDvxvlPYP3IAhgwCJZrTWH9yALoH+dxbYWTmAP+Bdl+M8gOrgifBiCAVRjWj6wCyKnrYW7IAo4JY4phOmHxOEvDGE7jy+NPHo7jOOFhhaeLllu/CQKDjtGWML5ww6Mftl5O8qVhMIwNaSfGagfbKQ2cq08PRw3DvRL5gDHhUceY8KhjTHi0QfQv3WxwqZwG02wAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAU0SURBVGhD7Vpbc9NGFD62bMWOLzi2kwJ2LpAWSgt0IEBvT33tdKYz7Vt/YB86w2/gpZ02hYdOAk+FaSBpIDeH2CW+yz3fareR09iyoks8jr+ZM9autOv99O3Zc7RS6KeVtQ6dI4Tl77nBmPCoY0x41DEmPOoYKA7Xmm0yaLjDdZhCFItqstQbtoRB9vubc6RHwtQZUs6hEFGjZdDDp69sSdtOaSiraxpFwmGKasNpGJvOv4PMwoF8uDOs0low6Bhtp/Rhs0U/3L5CUZ7SwPPdCm2/q5KGeXSGaDPBmUSc3s+nRLnZatOPK2s0GY2Ici84Jvzryx36c6/C0+hsCbeMDn2QS9Hn89OiPChhx2EpzMqC7Em+FKRhDBiLUzgm7BYGT8U2qwPDcdAIlDBIxiIapSeiwnCMuiARGGGsom3DoG8/mqWvPywIwzHqgowCgRFuspK3Lk7J0hFQh3NBIRDCULDFSt6+9H/CqMO5oFQOhDAU/HgmI0tE7xotqnK4U8A5hJkg4DthKGewgveKOVlDtPJ6n/7Y3JcloqVCNjCVfScM5a7l07JkhqXnpbIwRTDE8fT6dDoQlX0lrHx3yaLuKqsbDoWFrbw5Uvnu5VwgKvtKGDF2kdM/PM0orG69pSgyNbbVN29lLYnsCdf6HZd9Iwyl6u02PSjmZQ3Rs619fkw3p7AwWadwv5ATbfxU2TfCeJpZyCSFcgpP/i6RxmobrCIMx082SvIskc6ZF9qgrV/whTAUarQN+mzOfJIBXuyVKaVHKMmWkIbj1ESEz1XkVUQPZnOirV8q+0IYCs2mJ7u2WxZzafru5jx9c6PYZaiD7ypM6lEqclu/VPacMJRpskLWldkpltiX0YcfKntOGItsgRW6ENNljXNk4rrow48F2/GOx/KrXXpRqnQtRlYgrOC53BSn0xWS6qzaV1feo8sXJkV58+CQHv21RROWvhCLeVj/9aH12FnBDFjMpujTOTMK+Lbj0Q/IouLst1enkrQwlRAZFkjCH4UJyaz3V24GyPO4Fm3QFn2gL683CTwjDH+r8V3+cn6a7s/mxQo9l0mIemzFmIYrrYqZdeo8rkUbtEUfX/Av+vTSlz0jDPGy7Hv5REzWEP28tt1z6p+EKE//X17uyBLRdDIm+vTSlz0hjPE0OENCPqyw/U+VyvVWl552gN8e1BrctiZriO5cyrK/ssqy7BbeEOYpl+L4WZCLEbC8vifeBiCFHBS4Fm85Hm/syhqiIk/xJPft1bT2hDDe69zlZ1qF0mGdStW69FlnQJtdtGdTuMN9I/vyAq4JYxXVtRDN86qq8Nv6DocazZG6CmiDtsvrRyovcN/i3ZEHKrsmjDuPFVWhLHyw3jN+DgK03WI/Rl8K9zxS2RVh3HGocZUTAAUoE5NJihtMcB+/b+zJkpmLI0Fxq7KrkSHb+cSyE4nNudeVqoipboGXdZvlQ9Gnwq2LGfGfbnBqwlg1xS5FNkl1Tg7wfLvMvou6fr5rjcv9YjT6wPnHFl++MZMRbyvcqOwqlwbpGq/QZiQ2CVhz5+PAQOM84Igk2mK1qnyzes0I9I82aX4QwTGuwxcJTc63seEXeC4NFZDvxvlPYP3IAhgwCJZrTWH9yALoH+dxbYWTmAP+Bdl+M8gOrgifBiCAVRjWj6wCyKnrYW7IAo4JY4phOmHxOEvDGE7jy+NPHo7jOOFhhaeLllu/CQKDjtGWML5ww6Mftl5O8qVhMIwNaSfGagfbKQ2cq08PRw3DvRL5gDHhUceY8KhjTHi0QfQv3WxwqZwG02wAAAAASUVORK5CYII=" + }, + "95442b2e-f15e-4def-b270-efb106facb4e": { + "name": "eWBM eFA310 FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "cdbdaea2-c415-5073-50f7-c04e968640b6": { + "name": "Excelsecu eSecu FIDO2 Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3LWFiODctOTFjM2IzZjBiNzhlIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZWMxZTg3MjEtNzM3YS0wNTRlLWEzYTktNTFkMTMzNDZlZTI5IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjE4NWYyYmYtODVmOS1jZjQ3LWFiODctOTFjM2IzZjBiNzhlIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyMTg1ZjJiZi04NWY5LWNmNDctYWI4Ny05MWMzYjNmMGI3OGUiIHN0RXZ0OndoZW49IjIwMTgtMDUtMjNUMTQ6NDA6NTUrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/0VxRQAAGfVJREFUaAXVwXfcn3V97/HX5/v9Xtdv3Ds7JJAIAULYBZmCimDVDlftw23HqYuqPV0WtdbWR63nVG2rnraOtshDrRUfPR3WWS3KVhAZYQoEQkLWndzzN67r+n7e504iKNWO858+n2nuisS/J3G8YZeZ2ZTEImD85+ROO0ZSUfiHJP6FHyIEWBjAwzNw6obI3CykCGaGJNyhLMWwgnropNJICBNUcooi0O8b+xfF6PLAqIMcGod2W+zYD9Fg49rAgb1i0TJTHWGCuo6UheEJdi9mVrSN8cKYq42d+8SKCSO2gAwdIBQQTPx7ZlDVdkkWbzTZcKTI3dhvvrGlueM9d8UTX0Rr+jmoyYCQOMSsBLpAAjLQRxpgxo+RAmlr4ocIZheGkF5lBpL4rwhICXLDfH+gDxeFkHgCCeSwf78hEz/KjMPED5IgRXuRuf20pYBZQ72f7StGH3YmTvxFMhcgAwliARLgGWwGNAfWQqwmhshBcn4sGOA+l8qCxxmQBU3DSZIj8V8TYFC0jYUFbe31dP2y5ZAzTxAS5MZAgPGjzQBB1YDxA9ZZ0KkmcEHImc93Lvi3HfHIkqZejTIgMEAO7l8nxk8h3YLn3YQ0jusM1LyOEM5E4seCgOz/lPYcEI9xQTtxxHg3nukYIL5rEdgOCCj4fgYSsR5qRaejq0Jiuqp4ghQNLw1V4seFAK9FMr5HQLTjQgybMciNg7Hn1pWXfOOh6sSL8PkjMQdLYGGawd7fJXYvR0WfEMAC1BWE4lZ6C/9Mmf6OcuTpSID4kWUG0m7Evem2bc5jho1YOxmPOnMTp2aJ7ICBiY8J/T7QAkYAcZAAQ8Eoc0O2yLbRUUMCM5CMdhv2zTlkI/JjRGARQhHIjXiMGcdKGneM0jKIOx6pV+/LZucj7xAMSPvo6xV49QXSOMzNw8gEdFowMwMjY5DSXprmrRT6B4xViB9dEktuJNqOtHc+8Jj+EDpd2xTajGgAGeMgd/9nYE8I4IIQQCwJgIMLXBANmgySkR2K4Nz9IDw6LzYfLQrjx4YZNDX0ek53LCBxSAp2jplhghY1szZx01XNBXMEthAqQBW95h006QvEEahJtMuXUMQX0FRX02p9hCLNowCersf8PrBV/KfEYcZ/nzjM+AHuEAL/ITlgYMZhBq6bEQvpSUdGHlPVxBVjdo6y4RIgENsEO6JBlpECVLUTghFLQTYcIyMKQZMhG1QNFKX45j1iYtJoJUOV+CEMGAECMA+I/w8CXGCAO1jkv81YIsgOEoeIwyxAXYm5/c6qlYZnaDJH5czJhIBMmOAh3/jlgXVWQz6RYDAYXstC/Rd0lkM5AvI3UHTfRwBqfx4jo1uBL2IR6gDZG0IABO4QI2DgDiYOsQRykIMZP0jgGULicRYAgQvMOEQCMyha4BnkPIEEFqBoQa7AHUIEBDnficjppElxiIDIms6YnZkbaDJYMDz73cgfmWkCRYLJCP0+WAAKHmeAZEgQAgTjkNE2pAgShwjIAozjgZ9BOk+wzsBc7AO+gvikxKP8JwS4GDG4KEXOEqzqtPAA3zHjC4Kt/BcEy4Jx8WibM2JkKooaeAD4CuLbGBQlxBEjZkGf9XVtm4hgCIzZv+XFDz0YNp6NLaxEDmXns0yZEyoo0xnI/oicoakhRMBeg3wTUkn21RgnE8QhrQ4og2cHbQf24qwi2HqSBRqBADMe5w6pgM4YDHqQGzCDkCAVMOyBHCwAAgGxADl4BoscZqAMCGILwjhUPaFswA6C7mFJmnlUHOQZWl1Wj4yyRUEgkBtlyT2tqAN754W5sWRCcKrgDLDjgOUGCoGdGLcC/yp4hB9GEOCYqXZ4bW7sRdF0FGaGIAMpQsCeZYFfM7N3CP7aQHwfATmrRPZLrcivYGyWWVeCtZMgl5rK3pSiPobzh8CA7yMgi1GZXepur4zGpg2rYlnXAjeUhDsPWeTPLfLH1UDafm+mLoyRtv3EZNcmqyxaNCBuvT6euwPxMtRv4+rRG9xIMug0MNQBLNxPa2QLuYFqAMTnA8/noCIAxiEhgucDLPY+TjP4EuNj9+DWJ4RANXM6dN/CyLKzWJwFbyBEQBBLUIDFmQdxXUcq7sTCgGH/KPpzz6AzehIGNA2kNnjewfbbPsrY6vtoTz4fa16IBcgZWiOQ60fYfv+HmFhxB93Rn8Pzy3DdjrGdJam7MXCQBEXkDDPGcgUWwXAGfV1fW0Buay3y87g9v922Ew1bITcwgSAFQ8Jj4H6ZXVFLHwBm+S4HArx49TJ7R9kKxw8WwQKPk6BsQQGWzdYXo/GjdZOjMh82DpMgJjtp9UT8391kF+eGokjCJbIMlxBYrnVku2tvMw9HmvJrBQOWOFAETlnVDh9sWbigccNM1BnEkiAkkLEhBHt3GWwVmd+8d5vzxe/E9Myz7cyLz4fqESiV2Vls+PyeYm2PPk/FMsgHDPozWICqgm7nATy/gNk9r6Eon0d79Ek0FYcICAHEEoEPv8qjD7yTVcddw8R4QzWALBBg+WFmFr/KbHMFU+XzCAmygwUo0x72PfSXPHDn37LlKQ9h1idEwGFm1yo6x7yVsvtG6hkwoDP6NhZmLmfZxhYpXYzXIAGCaCC9i179FzTXQTrhQspN4IvfAuZZkrpdcZCgE2VnezZcImK0Onx1dtb+Lje6eNUK+2DCjq9dhBC05ADSiAXKVjSaRjQixGDHgr3T4FnAr0p82wWdyFtbI+G3TTbeuBAQgBAN5PMjLT53x4O6etsC+84/wdZOYi9tiO8yy7ci3chB4txWyz4S4cQiQOg6vR57TFyVgjyYXSRY1QAOdGJ8qaRrJPtoU3PQuSnYFaPRNmWDjDDYWdV+vRnZ4Gwz22BANZSVnfiqo47ls5POVfPLbO2KUdtMX2AGBQw6E9c0d+1dxdrjNtFOoDhCZ/957HhgK0efC6EG5x4Gi79OSh8gpKcR/dcou6fQn4fskCJQ/z3Ub2BqzU6aPowsO5bh4AJcu/Dmq7QnBvSZZ/vWtzN27Gl0JzcyWATZ9VRzb6bdvobN54qiBWqgGoIitEf3sOfAmxi3SLd9KVV/F63uVzj6LIjFOlRdgAUQEAMMq3vJdhVr1kJuLcMmn4oqoL4ZPIORGHCIGVNEThJgBtn9y8MBrx8ds7cFhXd2ohg2fmPO+nSQ3Qy2D9NkU9kpi42/oGyFi8pIkAtvxMSYnR+K+AkLzYtG23ZBuwxvyz2160aYQZFAUPV7/qmisD9nVLf1+vSne44sQNYVjeztpfHURn4TsM4svM/EiSHBTF/9hUX707Ktj4602IXIN9zVbJ4ai+/fcnS4sBqIxlW0Y3zdvgU+um3ajzjtKP4MbFMtkGnOs783hPDJEOxRSRgciXgbxksFlqKtaKf4wv5QV516rJ60yjmh2m9YEJTsfo9e/8h9BzaewRHzU4QCFFqE8Aa8uomiuIWmD56hLMDig7RHHuSWa7/EsP9RTnn6s4gGi/W1yN5IHOykM7GMhYU3s7j4UsRqilAgPk6Ov0673stR628nhxvI2kh3/CbmF1+LuI3xNeDh6VT9VyGORPlmGv9TJlbtxID54V/Saj8XfCdzexexNtTVWUTfgBmYQTDoDXfQ0zYmWpA2noP7CfhgHyHfjomDkjjMxPpAOA4Dz9wg8X7V+r2RTnz5Yq0Hds/lPxwp7TPBmOO7gkHlXHv3w/6xiSn/+VM2pbdXs/Ykj2I4EKEKW556UvHlmJioemorc0grQQOPHhj6W2nsb8qCx8UIMRi49tdZf1AUXDBWpomFSr9lFs4JCAvM7Zr1S/vzfHzDesMMEDRut873mrcop/cEWB8DzXRP93/qOi/OPzn9amvUnrwwC5ge8tpfBXyNJ7ob9DuYnWjYaZ7FYrZNMcNK2JKCjVdmdBnAgBsf0hHb2LLudaQDI1QVyKCz6mSOmfok7n+M/Et4/QitUeiOgzcg7WDY+z1yPomiXE9jf4hpB6b1pHg54yufwXAAZhANXC+nam4l8B6649BKB8gLMNd7J5Vuo4qREbuMwcJvY2EMi1CMXoSqDthlxAAdzdI0eyk732I4nOOuu2H96tNZtTwxrCAYxAQL+2/CrM/oauhVT6ZVdJhurqetA3QiOKQUje86xYwpwU7Hr20ne0v2dG4/6+vu/ipgG99lgFhiHNI4vUa6HPdv7hvwibFOODUBuRHjIxyRHeoGgkEMsGtG387B31h27GoJEODQbUO3Mu7dnlnZEWXBVLsdO5Y5Xh5eoCiKCDNz+UPT+/zjrZSQwIA6w9pJZzD0awfz+eeSaSwmcpXZNTVqp69ZYb8iB8+OR96dUvxaMEYlGWBLWJKBA3J924zTWOKoXDSnK9uYJAQEgwPN6NW7e2ugzdmQQSwR4NDubMb9r8jFVqI+AfYZot+H+nD0aSz5Bsq30BvsgvANmj3gfhRh+TShuRJ5BYiGAhgh6B6KBAasWH46X7/yc1jrK+x7ADY+8+XE+AcIwwRiSYZ2+UtIZ1A3MxRhAmkzln6fbdsaRIeiOJWDDJBDw4D22LcY9mB2DkJ6MrRgqnMzTX2AbByUkFjSwux0CQyfjm7PDeNh06DUF1p9vZzGpuWAQAYZMMAM3CEA3TZQsHWu1s/UMf/VUd1wSb+GQQ0GmEGIQApff3R/fu3KFdzlAjNQgGYIJ22AZpv40OfhwjMDzz3dLt25x+Ro4+rltiwPIXS4p13yJ1PzRrsFqQV1AwZ0S2M4BEk7DJFlrBiNxYvP54VkVizOiZBsEemngLME44D4nhooDM7iIAODxWgU0ThJAtwgwZfjJXdsDSe2CPkIVAMBMBDQDDkkdU7Euu+iHrwaeAmTozfgwGIFqIf4BKVP0x9C5jq8uY5Q8D3GIcpQlNCdWMnevcv49rc+yrLOIivXrmCyuIzKDRNgPK7JXeBczMAdsPsxu42NR4H78ZThFOoKMEDg7GB0fCsR2Lv/BI5YtxkL8J0br6O3PxMLDkpkDpqk0OkgYrCjrWMj9+3RTdMLevU4TK8eg7IFbpANhAhBWANmcMRyY6SA/oLYvMy31zle2Wu4hCXGYWZQNf73/YpLy5Z2lQFKjNACBehV0CmEAAdiyXndbnrp1unmj8pRzl7fsnbdwM55v3rdlvDoyRsMGjHYATPT0EqwcsKwEFEw3CCHQITV0eyiWuAGEUbKEH7aAQnMDAQOGGAsCYYAA5R9ayfY6Ql7umSU7RrmeHB7/aTbB1Pd55B7G3DLYLs5rA02AUTUgAtSsZHsL2bPgRtoHCxvAFtDsK0YMHlcC08ryL2E6hqL4qAQurgmiUXBsP8wvdYrqPbMsn7l1Zz6HFi25kJy3shgHkLgCQwQICAVsDB7Lb3eblathRBPYXbfCg6yCFZA/5E7Ge6+ndFTYM2G0xlrH0Nv5gBX/eO9PHw3dEY5KClw0LGBcCoYoJFOS+zcmT+9Y5e2r15hdDvG2nFjUIEBBphgUIt2aRy5yrh9u5jtiRPW8Ryv7HfdjIB4TDDDG3v4zl3DfWunjNFWoh2MJkLtEIEA9IYwVjK+6aj4f+gqnLZJN2XF1wzmhRVUDNnaTAMm6gXRzBmt0pA7VQ2rlhc0bmQXMQnPrOkNOc6CiIYHWBCqBMkMY4mExYAlo19l9Tms7WbT9dA/VrTt9BitW1XQsQyJ665ZPHUHzs9igxLxBoyrgQI4HvQBzKZwQVmA5Dy86yYqwfIWdOIFMHICsd0DQTVYhzVXgE1BmAVzzEaAI4EaYz/YDKk6FzpXcMHPPkznKCCtp9ofeZyAwCFyiAkCmeyR1LqdXPWY2QNmJ5DKhDtYgPbYkMXZ/4tFiCuAAz9BM4R+/0Y2n7OLdcdBKjkoyQBjM9A1RBbUiyyun7C7jl4LT1pjzC7AYAhmPEEwkKBqIDsEC78I9qc1jEeE+B530WmFX142mu6qc/6wAxlwAQYIqgxjHVa88qJwxUmrwmmPPly/eqodDySz5XUjYm3FiraWz+4WQSKZEVqgisMETaOOjGyoaHfFcNFGlBkLLDELg+x/Hcw/UgQ7KrsiQg4qZHm20e6W2ZxxSLdpvJ2d+wrs9TlDLA0GkUU1dzQTu6DiGJLNY3wWtA0MpPuBS8HOBYEE84t/QtH6OKuXQf9R8PZTaY+sYvb+BYYzMPKkfRTlPmI8HxzMQAb14MsEu5JQ3IL7y4iD80hjs7hVTO8B91tot2pSTMhABjSQ/XMU5VfBd7M42EIIl7Fm5RyjJXziz6CutvPcN2R6/UTTh8X9H6fV+RuqGaA/Tq5+gl4FqfUNLvz5/aQCJA5KJloW7GQzQxImY+j61oYjuNbN2DcLGJiBeJwBJTB0QQrW3bDC/qAswpuGtSXMOcjEfhkdoCPAXWPHLEvvne9jcj5iAee7hKhqe8bxa8L7WuviKffdnR/+5j360nOeTphMigxAYJV4aoxWFoTKlUEGBnII0X7ZjJcHVAmb2D/jfzbRsu8oWd+zuskgi/Yg+52jId6JGWYQgeyBPZXO3dANFwfRdTEm+TtapR8RzJ6R3eh0wfY3fGbfebddc+zLVlFrI4OqDWqDwAKgA8Bbwf8nKQVC61NUM59h1SS0OtAfvZii9QJMsLhtGckgNnNQ/jLKd0A8h5AXqPt/D91PEFOmGXYJcRliiTajZgr3abJdh/ROxG+hPEWIcyi8H5p3I1+kbqA//B3WroU7bzjAo/fD1BGw7bZPM6yOpCjOoan+lf7sB2lPQQR6u09gZORkHDD7JtUQqiGPSRaYDGZPFocZwkyr+xW/GQwrjEI8rhWMZYKVwOddfMhd58TC3rlqMpxfu2gaUQSjct0WsFcX0iuaaJfKRRa0IqNlN35g6P6zLn0O7CGDo8GeEYM9nRDG6LnPzuc3bZzioeZAXqbxsK1VhOXDSpjZBaXCR8z0Boc5lrizPJq9vSzt0ioTOy1jUGn20Wm/u73Btrfa3D+YtZOzYDTZa3pVmBs29rutksrMkBhPQb+4vh1+TzBlBlm6y4y3J2OF0BaLRr2YSSV3PbjqKV+bmVv3U8TekZgD8dm4303OEAOY/RuR62m1CtA81X4IU9BUmylb78fKZeQ+LH/yZRTDW6mb/eDTiLeT2qMMFobM7x6y+hTIfjTW/zgxnYsDFi6iGZ6C6d9opYzxxzS6imZwBGOj91OH2/DgZIdW+fsU6e20OrDnoROpdSWnPg3WbNpHtrexsDBCqzXHyCQ0DiHB/PRGxiZXYPVecvMQMr5fGhnV+oV5Oy1EDnFA2HGlwluiAcZhxiEu7TXZfULHhEKXE3ha5ayihmhGA9RZ/+TGb7jn78j9ESxeHCwcD2KYRTArkoXnuPjJAH2DtoKlgiUyWPRLJzv6h1gEFqfZ/8h2/c0Jx3NqUZJyA2Z6hdAWI/yrRLdT8EzHNsug0zKiaWeKegnGLQMpDOa5ciTYybULi2bdMv5GnXWhYVeDumZ2tsxOG41K2aGW3SDpJRY0INh5YAgDBwL3rIr7Fqk4DUtgBjG+mex3In0RM8iCfjNgcGDA7COQa5C9iFi8D1tYj9cgQWfiEurp9+LVH5HCvZg5+Bz9Piz0l7GOX4D8FhpbjsQhRiIW76YZ/gIp3oXUYM31pBLm52FQQXtqPa3wv5C/FDOYmYbTnv3bxPYOegsfYd2xMKwyg2qelj2bOh+L6y9ot0RafRG5BuVv4HoYxPdLuw9w3nhbHXcwQIIiQpFgWAl3sMAQ8Yjg9ib7rkQYiYU9H7N1LhEEjXDQ9YtDf380PtNqBc9AI+0I2X8ppXC5sGMdIQlxSBSMGlCYMWg0bda8voU+7dnwDJ0Iew7oY2saf9rqkfhzvVknm8zgzGDhTAEREYNRZdEfautYl1enxHWGyAfcLdtfxzF7Vtm28/p9sSSmZOe4cw4YBzlGPwt3/5cQwpswtg1rJmIRnhmCgaATKmY0ddvn9TwoOQvmOURaTQyXI/8Y8FVcDzB0GM6vYzg4hbXHP5MmP5O8WBITh5hBNQ90foGyfSGevwi2C29Ed/xIyvYFDBePBkpCAnGYZ7B4FmX7M8DloOsw7Samkrn+MXj9FLrpeeDH0TiYgWdojXao6/cSeDbD3q1kb2iXx+P2XFKMiJ8m2DixPA014NxMtlmMJ0jb9tnZZxxnDOfkBBQCw2GjhcVK02WyngVlyeYxTHBcCuECC4zWWVni3mS6rwjcOZe5vsq6Osr2SeIxBpi4buD5xQG7LJm90MFSMCRwiSLSm6n1jwuV3ruyxc0skURrMtDpGidMsZCC/aqyzwq9MkUrzI1GAoxa0E7a45Wu7A/1J2PdcD8CBKpEu9SOnMPL983z5xNtPSsRGGYoAkjgEgm/Z99QHy4jl3eD7R9UjmACOBWJQ8TiPlv+2ft13BbE6YQaCDXuhtkaiuLNoNeQwn5GCqNYPsmyI8aIRaLuQ64bQiEQhxlgEexoTK/joJyh1YGRSRjMC1ETAk+kQExbUH4XhBkIs7hKppYvw2wEr1nimDWAESIMemA2SozPR/58YoQEuACDYJcgB3OWOHAdQfx7afPq8MFqUZ/EaEAKwRZ7feYXKy0eudKyGpsaVkzGSNtgBOTIpptGM2ALKXEAmHfRuKBgifFEBln6lsP/kOuKYPaUoeuoEGwYpHvqxr9eK9zkMDS+TzSsMDoJAuz2rDcOh/nvKsVnWNDxLQiYpt11izJfk7TVzDKPMSAABiHw4N45veThPf6TW9bylLJgw6DCzNiZTNeY+HqWHhLG9EJN3YiU7MBIaa8RgSAlEotfqJ91813941fQ7b+SQMZVAYZkmLWRuhhtygQh1BiLVIsDjExIgPNEDQgDEpAIBrluyE2DmTCWiB+gJgAdjBHMEpKIcQj0aOohZg4YjzGWyJAiUCAHUQMNB0kRcEQbbBa4iR/i/wH3D5PMpd2t5QAAAABJRU5ErkJggg==", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "bc2fe499-0d8e-4ffe-96f3-94a82840cf8c": { + "name": "OCTATCO EzQuant FIDO2 AUTHENTICATOR", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "eb3b131e-59dc-536a-d176-cb7306da10f5": { + "name": "ellipticSecure MIRkey USB Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABXCAYAAABBaAoIAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAADfAAAA3wBJqFJIAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABenSURBVHic7Z15XFzl1cd/57kXmGxmcYlpGo2ahChL3H19P1bjklp3rU5ggFBqrbRGIUltgMTW0VqBxCaQuFerDcsMjMtrXRq1jahv1VZ5NUBiksZYaxWjMbsBhrnPef+AGe69M8MMhGEA7/fzmc8n9zzbITPnPtt5zkMYItjteccqSmcKC55FoNkAZgM4CowJAMaCMBaM0XFWc6hyEIxXNE1Z7PGs+3e8lRlJULwazs3NPbKzU86V4IsIuAhdBmFxeOzWfEq6x7Pus3grMlJQB7OxvLw8W0eH72oG53o7tUsBqHGz0JHJJEXVVgLIirciI4VBMZCMjIyTiBJ+0dbR6SBgwmC0+S3m4ngrMJKIqYHYs7NPUSRKAGQCHG1vsQugLWBsZeKPifkABL5hYD802scKy1jqPNwQEOeC+W6daFzclBmBxMRAcnJypmgaVrJkBwARIfsWBjYI0Aafr+MNj8fzVSx0GqlkZuaMjt9McuQzoAZit9sVVU28xafxbwCMD5uRsZ0JVVIVVZ6qqo8HUgcLi4FkwAzEnp19isKoYsbpYbJIAj2tCa6sr6l5CwAPVNsWFrFiQAwkIysnlyQ/CGBMiGRJoKelpDvddVUfDkR7FhaDxWEZyFVX3Tx6zJhvHgBzXugc9Lom+BZPTfXmw2nHwiJe9NtAsrKyJkr+5gUA/x0ieTeIS9y1Nb+HNZSyGMb0y0Cys7O/KyVeBnCKOY2Bl6XPu8BajRocNBWbFcnF/mdi8sVTn5FGnxcIMzJyZ5GivQrGcaYkH4junD1rRpnT6bT2KixGBH0yELs9d6qSoL0Vwjj2E8R1LlfVhgHUzcIi7kQ9xLLb7eMVVXsxhHHsZEmXu+uq/m+AdbOwiDtRGYjdbh+lqEkvADzHlPSJIJ5XW1fzzxjoZmERdyK5gQAAFDWxEuDzTOJdmkKX1tbWWsZhMWKJOAdxOHLmM7jOVOoQEy6uq6l5J2aaWVgMAXo1kKysrJmS6T0AR+jEGktcXldX80psVbOwiD9hh1hOp1NIpioYjQMgussyDotvC2ENZMu2bTcBOMckbtA6O+6NrUoWFkOHkEMsh8NxFENsAXCkTrxL8yWkeTxPfjE4qllYxJ+QPYgElcJoHACjyDIOi28bQQaSlZV1IoHyDELGu7Nnz3xycFSysBg6BBmIlKIIxg1ETQi+2fKvsvg2YpiD2O25UxXV9xFASTqxy+2qscLIWHwrMfQgQtUWm4yDCbJskHWysBgyBIZSc+fOVQnI0Scy6AW3y9U0+GpZWAwNAj3I5KlTfwBgsj6RGKsGXSMLiyFEoAcRknLZeDr2k9mzZ7wx+CpZDFdK0yoWE+hyAGCiV0qaClbGW6fDRQWAq2+8cRy3dVxlSCFUWStXkSlLr1hG3OOOIxnvl7QsquutTK/1pVUuJfAk/zMDHxU3L/p9uPzlqatPlkIsMAiZvyppXrQ6dP1r7iDIsZH0kETtxPiKmLe1HRz/pvNfP26PVIYgTgb4EgAglv+JlH84oALA6EPe80Gw6RNYU6rio9Lwod5er+zY0upkIMEvI8L+sjMeWV/cmL+vr/WVz1lzNksuZ+Pi4mYAYQ0EhGRilJiE2wCENBBA3sagYyLpQt2DCSaCbdz+g2WplY8nsnb3kk1LdkcqO5IQAEAkLzKKeVtd3bpt8VBoOLFjc+tU6IyjmyPgbc/vT30seVkI8fH9qWuAGQtCoVcoLWVpa9PircxgIgCAIS7UCxn0WnzUGV4IhcL9eJc4pz9hC5MWkvL0imQAV4VIGnPv7PuPDCGPB1MAuX4I6RNzRFZW1sSgo7TEDfFRZ3ghpQxnIJOTxu3P7UtdzFSEML5xamLnUOhF/HxHJGgr4q3EYKFKKVJBbPhiEoR4PV4KRYPdbh+rqkmXSvB5AnQsM48CaCeEbNQ61fWDdg0Zienh4uIRUFRvr398vme+Fqmae+asmgqJ7HDpmhTHA4hZUAzBfJVGiX8zyMh7DEA3MOMOAObeMGdFygPLl25aOOKdV1UiJJu+4t3V1dWt8VGnd/Ly8ia0d3QuBaGQmUcTAAZ3O8wwwARF1bRMR/azgriotrZ2Ryz1IebjOfyZzBN3fNh6PYD6SPUksLqYwYnh0oUIO5QbGEgcKGm+ZY9JugfAb8vSKz8Cw2VKS5RK5/cBrIupXkMAwYRkk2xLXDSJQHZ2dlp7R+f7AEoiXOapALhBMjU5HDmOWOrEFGECTVjG6MWEAJSd8ch4Zr6p13Y47FAu5hQ3FboBvG+WM3BGHNQZdFRmJOu/QQJvjZs2YcjIyJkjJb+Jvt2eNIbB1RmOHFudq/qJmCjGmB4h7MWcstS189CC8EeUO9sWAhT+LhUAQIx7kAgQ4e/MOE0vE4yj+1NX6amrp0NTZgTqIeYTkqc0hBuKOuEUo+ZMOlNq8ntENJVBCQR8JcHveRNtDc7G/EOhyq1MX3uCj/kkfxtLmwo2UGDxuhf9UlbNgFCnA4CQrKlEPFU/jJagj/r0F8cYu90+iQQ/x8HGsQPM6wDxPjN3QqGTiJFhCk8kCPKhjIzslrq6mncHUi8GUzmtmRYpHwkuBkIbyJoZa5IOMd8aubX4Ggg4xLEI6nsM4PL0imTWqAHgYwM1g2+d75n/1+AmmcpS1/6YiO9gyScQdb2JqPvHKgDYvO37S9MrV3ck2FaYDUWCJxP4VQBgBsrS1s5DM/4SSUcSwgXwmQDAgv5HgI0/PNE19hwyKAlJd8C4F8BgvlfzeU92u2vvcrur/1RXV/Pnutrq+92u6u+BYQdwsCc7JRHhAQzwldcrUx6cjODJKwDaZHhkXHhveoX5bD8A4JtR/CMAU0ziEFdFcFwNhAlnhxD3aad8xamVM5npNQAB4yBwQVHTogfNeZ0pD4wtT1v7DBE/DuCEXqo9ghh32rwdb94zZ9VUfUJRU8E7AAUWNoj5Z5F0LJ+z5myAzuwpIx8UML2ZJdHBoJJxwuFwHAWWtxiEzPe43bXLPR6PN1QZt7vmKTCuBdDzhiOclZGRPW8gdWNFmx5CfEiCl5qFomsJ10C9vV4hxu0mcScTfhGi3iPLk8vjcjlnWdqafDBONcsVyW9FW0dpyqoZUsNrML4MiouaF91vzltvr1dswucG+FqdeC+AaoB+BaCEgEcJ9GlPMp+uSuX5VeeuGqWvi8GPBB4I15iNyAxL+fOeJ9re1rL3ryoAg18OST7QWyWDi7gCQOB8CgFbW7/4/O5eCgAA3O6av2Y6sp8A8NNAWYEMhBnq9Acp5fH+bl/HJ8uaC18qS6t4T/8mAnBt2ZxVKcUblwR6l4+2fP5DIpppKl/dkWB7w+YNdnvS1FHTELJ3OXwk5KzStMrAi5EhE4npOEHiegbfEKJI6/ikUS9HU3dZSuVxEHgVgP7HWVLcXBhyL2XH1tZbAVzRI6Hft0tliXPTQsOL2znXqdq+nrgEQCm6RlyneQ+odwBY7s/TIdVam/CtRFfoKjVBKjcC+E2odkvTHpwIdM73PzPkg044pQrAYHVE1BbF3z1I8AWmkdEfGhoaohr7CuJHJFOPgQB2hyPbHHi732zxNk9P9PptlzHm0Dgcs3PKpwBALEqZ+GlddoJUfgHgxh4BmXsKScTlzsb8Q2WplbtAOMrw93Qt9cbopi561LhQIwDqXkIPlRtYnt+Y3xmp1ntSfzcNhNcATO8pzMuLmxaFPITnPOOR0fC2LfN/5wR2FTUX3hwyb4PTB2BFWVqlQJeRAMS3rT519crFHyzeCwDOTQsPlqZWVhOhexRCP623198bakGAuPMnoMDqaFuSlH8EuizPYBBdm25DAwn6jv5ZI456oj1r1qz3AQS+RAbGMXDJQH06EttnHBi7D12f/fjimM+w89jPRgPA0pbbngXQbFIppyyl8jgAKE+vvBjBMceeKmpa1LWCSBS00RnPpV49zHiwqLkwilVBmqqS+hqAE3tEvLy4aVHYuGq2zo6L0eNI2dkp5C8jtdJ+5J770DMfGtfuo0uN+tID6N7NZfC0j7d+foWpCjCYQLrRBqPG75QpYJjQAixoyFxETyDDpaBCKiGX9ELhdDolARFdtAeSL45ufQsAupcTzW/JBAgsAgBmBM1JBMly3WOQgVCkPZfYs5fAt5W0FC6MMv88ACf1PNKdvRkHAIB7nGYZeOeOjUs+i9SIs8HpA+N5XTvn69OXbSrYDEaPlwBTkCNpWfqaSwHM8j9LwsP+f6sADqBnZQGCOeJZgcGCIXeRbojFQjsBwN+jKbtgwYJjOn1yMI39C4X4Mf/DibOn1O3Y0nondP/xAG4uT698kRmXmMq+uLRpcWDFhUn+m0z7ixS/lawPGaiwKbLeP3TpBzsTpS9oQh4E0zT/103AOWVpldG61gdWE4kQPBEnPATgPABg4Acr09ee8Mum2z7Wtftzv8sQgd4ubi5o9CcJEAyTcglMjFKpmEMkNuqfBYtroi3r8/F1BgHjHUF8Ziw+YO30BFXM0F8FMd8zX6PgXmQMM56BecmZYXizktSv0AT++unR/u19RYDmFjcXEkK7xUwhifX9MA7972qyVygvOVMe6P3lS4Y40Ino+i1G8wlMCxiYYK62Xe55CuAvux+FDzIwnCpLqTyOwIFhF7M0LDurzPQZgU/3CwT4JAwRmHg9Me4MPIPtWVlZ99XW1jb2Vs5ut49n8HK9jAhVkcoNNBMSbdV7vO2/hn6Sag4GDmoobikwL5kGz0HAA7bAEA41sXOhz5twAYyxCSZAoMoJ54VORH/ClIFnCNwOBIY059gU35+c05+4PNzpRCLazRxYGDgAoM8XwRL4Y7PMucnpLU+reILRNbQlxk1rZqy5q2B7QQeI8gFWupXe1X5w/FP6sioRtoJ7ziEwyOybFTfqamreyXRkN6LH70eRTE/bc3Iu8VRXbw9Vxm63j1XUBA+AwC43AQcSEpR+H4PtL/mN+Z3l6RUruyeKISGWQeNySfJTEbx5PWXNjDVJBdsLOgZaTz+3N96+qyy1sgAE8//V+UlpEwrRHO6UYjAE5vbmvbeMSptwBIO6fOIYFyaNO1DnnOu8vnsVygCz4cf9j+LmQvNQtN8oPvUhn6rdji5fvaO/sclrnSnOZ0H8E53Sj5mNVxDD7Hs1e6CUGggIYimMPuXHKxr/w+HIXtR1lqWLyy67LCkzM/sGRU1oBMiwKcjMd69bt+7rwdJZz6hD4nEA4Sab7xa1LHrVLJSCQ7nri3YbvjugyoWguKWwHoynzXICla5MrUjvS11OOOWExFE/YsILPfXw1bavJz7hhDPoDUDSsE91fvlpa79jzhOynTMe6c15FQBw+4e3fgJwoH4i8TObmHgDenpLqZB41FxOcLCBTMrJyTG7P8QNl6tqAwH3mcQTGVgtmb7MdGRtzXRkt4yfMOlrEDwAzTLkJLxyxBFjKwdPYyPdb/yQ4ZMIwb0HAPg27m8FEOQpoMXa7b0bmUA/R/DwJkkjUdvXk5L5jfmdSWO0+QA16MQ5SakT15rzth29500Afl/ABPbJ+0MZkp7y9DX/ZfO2f1mWVvlMWdray3rNT+Khnge+AAxn4InwkmHi3o0QQrYAMIwtO6W8oDelBhuXq6aIgUdCJKndBpECYExwMr2udXqvf/TRRyNuasWS9kTbw7pJop8P25r3/SlU/u6xflCvIwZpL2TZ+wVfEWhxcAqn2I7YH3InujeWvL2krb0N1wD8nl9GhFvK0iru0udzNjh9TPQrneg6W9rEp3578uqgF3a9vV4pT6/8ETOvR9d3fx3A95xiPyWsz92Jyce+xIRP/CqAoPdkCDkMFrW1tXsAMqwWkTSeUR8CcJ2r5mcE+imAKIZK3AGg9IvW/1zi8Xji7lvmbMw/pBFdzKB5gY/Uru5t0sshJupyEPdCipoLagA8G5TAWFKavvai4BK949xesF9N9F0GgzcA/bostdLgj1bSVOAixmM60XWKKv5Vllb5Slla5ary1IoVpWmV1Tu2tH7KjCcB+I8K7CPWcno7wTnfM18jpsdCJH3U0bQnpBuSCgAM3kDQ+fsTDzUDAQC4XNWP5eXlPdXe7r0JhAyA5sAYVaQFTC8qCu6vqakZUnGZljcVtgBoiboA4d9mTw/iwd0L0XxyoaKKCwBM0okFsVy3KmVVel9DAN3eePuu8tPWzmOf/F/4vXQJK0rTK/eWNBUGfrgTkmy37O5o9/a4iCARXRuP85gIIbqI/wiS1yxtXvxhJB2EVB6Twvdr6H83jIfDvay6wv5wUBSTmfacnCGzmqXnySef3Ot2197ndtWepfm8o8DacZpPOV7zece5XTVpbnd18VAzjn4hg3sQ0OCeC1n+4eJWIiwJkTTVS0rQhDYait6/7XPBNA/A590iIsbDZamVAUfB/Mb8zpKWwoWyy2mxt6X5/SDch0Rbqn6jtTeWblr4BRP0jpZt0qeEdZ1RAeDQ6MQ3Rrd1tEO3I6lqvADAHdE0Gi88Ho8GIMSm2vBHgtcKkGGIw1qwI2lbm7LBlmR0RydFhnexkeL7LFh//wvY2xY2BlpRU+EfS+esaYI0BvYAdXnU6pdrVZ/4baeqdc0VFQ47FF7aUvDRipQHztCEL7DrLQWClq+XNRe+BOCl0pRVM6Aoc4XEVBCPkoyvQGLjmDa82ddl7+5gf/rVOPeyLbeG1TXQW2U6susAzNelfTI7eeaJVvhRi5FEeXrF1cz0nP+ZJZ9dsmlRWCfYwFuBBcyhRo/fvG3bkFrNsrA4bPTOiox/9GYcgM5Adn722XoAOw2JkkKNPy0shiX3pP5uGgMBd3gSCDruayZgIA0NDT4QGXsRwhUZGTlzgkpZWAxDVFJuRperCQDsaUuweSKVMUy8VIFVMJ6hIFLYFDncwmL44ZzrVAH6sf+ZmB8LFzJIj8FAqqurWxn4oyEH44aMjJzTYWExjEnaNfEq9JyLZ1Ip/JUSOoL8VhTiFdBHBAEUIn7Y6ezdJ8bCYihDPW73IODPSz8o/Gdv+f0E/ehra2t3gOkPptrP2rp1+43mvBYWw4GV6SvHkMBeAnm6P1FHpw/p2JWbm3ukt1PbAhgia3ytKpQ2VANbW1jEAiWUcOPGjW2pqel7QYYLXUZLpjNPPjm5avPmzRFjnFpYjATCzitmz575OIC3jVK+QElI+lXIAhYWI5Be49Xac3JmKBo3wniOWiPQlS5X9frYqmZhEX96XZnyVFdvZwq6u0Jh4qcdDse5MdTLwmJIEHIOomdTc/PmlLT0KQToY80mAHRNWlrq8y0tLbtiqJ+FRVyJam9jVFLCIgBvmsRHMYmXMzJyZ4UqY2ExEoj6zgy73T5eqIkNhKBQ+LsJ8kqXy/V2yIIWFsOYqHfHPR7PPulTrgQCh979TALEyxkZ2d8fWNUsLOJPn29dysjInUVCewUICiCgMXD3yckz77EOWQ0eeXl5E7xebyAappRSut3uoEs3LfpHn/2r6urWbVMVOhegJlOSQsBdW7f+8y9DKa7WSKe93Xe+ZHrP/wEpf4tcyiJa+uWAWF1d3SpIzjWEle+GgQt9Gm/KyMoptBwcLYY7/f4B19bW7tE07zwwPR4ieSIxV2zZuv317OzstMPQz8IirhzWG97j8bS53dU3EbAApot4uuDzNIkPMjOzn7fOlFgMRwZkCORy1VQT5Fn60JKGNghXkuB3MzOzn3Y4HOdhgK9ktrCIFQM2R3C5XFtmJ886h4CF3HVtb3BbhB8yxJuZjuztmZlZd2ZkZAyZu0gsLEIRkze5w+GYzBDl6Bp6RTBC3gaiDQzeIJjfcLlcO3vPb6EnMzPnahA/pxO1uV01Ea8DsIiOmA517Dk5yYrGJQCy0R3FMQp2A9hK4C3M2EFEBwE6KEnug0b7WGFrj0WHgDgXzPq74y0DGUAGZS5gX7DgBKVTWwKibAyhOxBHKF+6XTWTI2eziIaI3rwDweampr0tLc1/njbtuxWjbKM/QFcM4BMHq/1vFYTnWpqbn4m3GiOFuK0m5eXlTejo8M2V4IsEcDEDp8RLlxHE12At3e12fx45q0U0DJnl1gULFhzToWkpQlIyE5Kp667EowGaAPBYAGMR8hYpCwIOMPCyIrB4RFz9MIT4fy9/yfbOhdfBAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABXCAYAAABBaAoIAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAADfAAAA3wBJqFJIAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABenSURBVHic7Z15XFzl1cd/57kXmGxmcYlpGo2ahChL3H19P1bjklp3rU5ggFBqrbRGIUltgMTW0VqBxCaQuFerDcsMjMtrXRq1jahv1VZ5NUBiksZYaxWjMbsBhrnPef+AGe69M8MMhGEA7/fzmc8n9zzbITPnPtt5zkMYItjteccqSmcKC55FoNkAZgM4CowJAMaCMBaM0XFWc6hyEIxXNE1Z7PGs+3e8lRlJULwazs3NPbKzU86V4IsIuAhdBmFxeOzWfEq6x7Pus3grMlJQB7OxvLw8W0eH72oG53o7tUsBqHGz0JHJJEXVVgLIirciI4VBMZCMjIyTiBJ+0dbR6SBgwmC0+S3m4ngrMJKIqYHYs7NPUSRKAGQCHG1vsQugLWBsZeKPifkABL5hYD802scKy1jqPNwQEOeC+W6daFzclBmBxMRAcnJypmgaVrJkBwARIfsWBjYI0Aafr+MNj8fzVSx0GqlkZuaMjt9McuQzoAZit9sVVU28xafxbwCMD5uRsZ0JVVIVVZ6qqo8HUgcLi4FkwAzEnp19isKoYsbpYbJIAj2tCa6sr6l5CwAPVNsWFrFiQAwkIysnlyQ/CGBMiGRJoKelpDvddVUfDkR7FhaDxWEZyFVX3Tx6zJhvHgBzXugc9Lom+BZPTfXmw2nHwiJe9NtAsrKyJkr+5gUA/x0ieTeIS9y1Nb+HNZSyGMb0y0Cys7O/KyVeBnCKOY2Bl6XPu8BajRocNBWbFcnF/mdi8sVTn5FGnxcIMzJyZ5GivQrGcaYkH4junD1rRpnT6bT2KixGBH0yELs9d6qSoL0Vwjj2E8R1LlfVhgHUzcIi7kQ9xLLb7eMVVXsxhHHsZEmXu+uq/m+AdbOwiDtRGYjdbh+lqEkvADzHlPSJIJ5XW1fzzxjoZmERdyK5gQAAFDWxEuDzTOJdmkKX1tbWWsZhMWKJOAdxOHLmM7jOVOoQEy6uq6l5J2aaWVgMAXo1kKysrJmS6T0AR+jEGktcXldX80psVbOwiD9hh1hOp1NIpioYjQMgussyDotvC2ENZMu2bTcBOMckbtA6O+6NrUoWFkOHkEMsh8NxFENsAXCkTrxL8yWkeTxPfjE4qllYxJ+QPYgElcJoHACjyDIOi28bQQaSlZV1IoHyDELGu7Nnz3xycFSysBg6BBmIlKIIxg1ETQi+2fKvsvg2YpiD2O25UxXV9xFASTqxy+2qscLIWHwrMfQgQtUWm4yDCbJskHWysBgyBIZSc+fOVQnI0Scy6AW3y9U0+GpZWAwNAj3I5KlTfwBgsj6RGKsGXSMLiyFEoAcRknLZeDr2k9mzZ7wx+CpZDFdK0yoWE+hyAGCiV0qaClbGW6fDRQWAq2+8cRy3dVxlSCFUWStXkSlLr1hG3OOOIxnvl7QsquutTK/1pVUuJfAk/zMDHxU3L/p9uPzlqatPlkIsMAiZvyppXrQ6dP1r7iDIsZH0kETtxPiKmLe1HRz/pvNfP26PVIYgTgb4EgAglv+JlH84oALA6EPe80Gw6RNYU6rio9Lwod5er+zY0upkIMEvI8L+sjMeWV/cmL+vr/WVz1lzNksuZ+Pi4mYAYQ0EhGRilJiE2wCENBBA3sagYyLpQt2DCSaCbdz+g2WplY8nsnb3kk1LdkcqO5IQAEAkLzKKeVtd3bpt8VBoOLFjc+tU6IyjmyPgbc/vT30seVkI8fH9qWuAGQtCoVcoLWVpa9PircxgIgCAIS7UCxn0WnzUGV4IhcL9eJc4pz9hC5MWkvL0imQAV4VIGnPv7PuPDCGPB1MAuX4I6RNzRFZW1sSgo7TEDfFRZ3ghpQxnIJOTxu3P7UtdzFSEML5xamLnUOhF/HxHJGgr4q3EYKFKKVJBbPhiEoR4PV4KRYPdbh+rqkmXSvB5AnQsM48CaCeEbNQ61fWDdg0Zienh4uIRUFRvr398vme+Fqmae+asmgqJ7HDpmhTHA4hZUAzBfJVGiX8zyMh7DEA3MOMOAObeMGdFygPLl25aOOKdV1UiJJu+4t3V1dWt8VGnd/Ly8ia0d3QuBaGQmUcTAAZ3O8wwwARF1bRMR/azgriotrZ2Ryz1IebjOfyZzBN3fNh6PYD6SPUksLqYwYnh0oUIO5QbGEgcKGm+ZY9JugfAb8vSKz8Cw2VKS5RK5/cBrIupXkMAwYRkk2xLXDSJQHZ2dlp7R+f7AEoiXOapALhBMjU5HDmOWOrEFGECTVjG6MWEAJSd8ch4Zr6p13Y47FAu5hQ3FboBvG+WM3BGHNQZdFRmJOu/QQJvjZs2YcjIyJkjJb+Jvt2eNIbB1RmOHFudq/qJmCjGmB4h7MWcstS189CC8EeUO9sWAhT+LhUAQIx7kAgQ4e/MOE0vE4yj+1NX6amrp0NTZgTqIeYTkqc0hBuKOuEUo+ZMOlNq8ntENJVBCQR8JcHveRNtDc7G/EOhyq1MX3uCj/kkfxtLmwo2UGDxuhf9UlbNgFCnA4CQrKlEPFU/jJagj/r0F8cYu90+iQQ/x8HGsQPM6wDxPjN3QqGTiJFhCk8kCPKhjIzslrq6mncHUi8GUzmtmRYpHwkuBkIbyJoZa5IOMd8aubX4Ggg4xLEI6nsM4PL0imTWqAHgYwM1g2+d75n/1+AmmcpS1/6YiO9gyScQdb2JqPvHKgDYvO37S9MrV3ck2FaYDUWCJxP4VQBgBsrS1s5DM/4SSUcSwgXwmQDAgv5HgI0/PNE19hwyKAlJd8C4F8BgvlfzeU92u2vvcrur/1RXV/Pnutrq+92u6u+BYQdwsCc7JRHhAQzwldcrUx6cjODJKwDaZHhkXHhveoX5bD8A4JtR/CMAU0ziEFdFcFwNhAlnhxD3aad8xamVM5npNQAB4yBwQVHTogfNeZ0pD4wtT1v7DBE/DuCEXqo9ghh32rwdb94zZ9VUfUJRU8E7AAUWNoj5Z5F0LJ+z5myAzuwpIx8UML2ZJdHBoJJxwuFwHAWWtxiEzPe43bXLPR6PN1QZt7vmKTCuBdDzhiOclZGRPW8gdWNFmx5CfEiCl5qFomsJ10C9vV4hxu0mcScTfhGi3iPLk8vjcjlnWdqafDBONcsVyW9FW0dpyqoZUsNrML4MiouaF91vzltvr1dswucG+FqdeC+AaoB+BaCEgEcJ9GlPMp+uSuX5VeeuGqWvi8GPBB4I15iNyAxL+fOeJ9re1rL3ryoAg18OST7QWyWDi7gCQOB8CgFbW7/4/O5eCgAA3O6av2Y6sp8A8NNAWYEMhBnq9Acp5fH+bl/HJ8uaC18qS6t4T/8mAnBt2ZxVKcUblwR6l4+2fP5DIpppKl/dkWB7w+YNdnvS1FHTELJ3OXwk5KzStMrAi5EhE4npOEHiegbfEKJI6/ikUS9HU3dZSuVxEHgVgP7HWVLcXBhyL2XH1tZbAVzRI6Hft0tliXPTQsOL2znXqdq+nrgEQCm6RlyneQ+odwBY7s/TIdVam/CtRFfoKjVBKjcC+E2odkvTHpwIdM73PzPkg044pQrAYHVE1BbF3z1I8AWmkdEfGhoaohr7CuJHJFOPgQB2hyPbHHi732zxNk9P9PptlzHm0Dgcs3PKpwBALEqZ+GlddoJUfgHgxh4BmXsKScTlzsb8Q2WplbtAOMrw93Qt9cbopi561LhQIwDqXkIPlRtYnt+Y3xmp1ntSfzcNhNcATO8pzMuLmxaFPITnPOOR0fC2LfN/5wR2FTUX3hwyb4PTB2BFWVqlQJeRAMS3rT519crFHyzeCwDOTQsPlqZWVhOhexRCP623198bakGAuPMnoMDqaFuSlH8EuizPYBBdm25DAwn6jv5ZI456oj1r1qz3AQS+RAbGMXDJQH06EttnHBi7D12f/fjimM+w89jPRgPA0pbbngXQbFIppyyl8jgAKE+vvBjBMceeKmpa1LWCSBS00RnPpV49zHiwqLkwilVBmqqS+hqAE3tEvLy4aVHYuGq2zo6L0eNI2dkp5C8jtdJ+5J770DMfGtfuo0uN+tID6N7NZfC0j7d+foWpCjCYQLrRBqPG75QpYJjQAixoyFxETyDDpaBCKiGX9ELhdDolARFdtAeSL45ufQsAupcTzW/JBAgsAgBmBM1JBMly3WOQgVCkPZfYs5fAt5W0FC6MMv88ACf1PNKdvRkHAIB7nGYZeOeOjUs+i9SIs8HpA+N5XTvn69OXbSrYDEaPlwBTkCNpWfqaSwHM8j9LwsP+f6sADqBnZQGCOeJZgcGCIXeRbojFQjsBwN+jKbtgwYJjOn1yMI39C4X4Mf/DibOn1O3Y0nondP/xAG4uT698kRmXmMq+uLRpcWDFhUn+m0z7ixS/lawPGaiwKbLeP3TpBzsTpS9oQh4E0zT/103AOWVpldG61gdWE4kQPBEnPATgPABg4Acr09ee8Mum2z7Wtftzv8sQgd4ubi5o9CcJEAyTcglMjFKpmEMkNuqfBYtroi3r8/F1BgHjHUF8Ziw+YO30BFXM0F8FMd8zX6PgXmQMM56BecmZYXizktSv0AT++unR/u19RYDmFjcXEkK7xUwhifX9MA7972qyVygvOVMe6P3lS4Y40Ino+i1G8wlMCxiYYK62Xe55CuAvux+FDzIwnCpLqTyOwIFhF7M0LDurzPQZgU/3CwT4JAwRmHg9Me4MPIPtWVlZ99XW1jb2Vs5ut49n8HK9jAhVkcoNNBMSbdV7vO2/hn6Sag4GDmoobikwL5kGz0HAA7bAEA41sXOhz5twAYyxCSZAoMoJ54VORH/ClIFnCNwOBIY059gU35+c05+4PNzpRCLazRxYGDgAoM8XwRL4Y7PMucnpLU+reILRNbQlxk1rZqy5q2B7QQeI8gFWupXe1X5w/FP6sioRtoJ7ziEwyOybFTfqamreyXRkN6LH70eRTE/bc3Iu8VRXbw9Vxm63j1XUBA+AwC43AQcSEpR+H4PtL/mN+Z3l6RUruyeKISGWQeNySfJTEbx5PWXNjDVJBdsLOgZaTz+3N96+qyy1sgAE8//V+UlpEwrRHO6UYjAE5vbmvbeMSptwBIO6fOIYFyaNO1DnnOu8vnsVygCz4cf9j+LmQvNQtN8oPvUhn6rdji5fvaO/sclrnSnOZ0H8E53Sj5mNVxDD7Hs1e6CUGggIYimMPuXHKxr/w+HIXtR1lqWLyy67LCkzM/sGRU1oBMiwKcjMd69bt+7rwdJZz6hD4nEA4Sab7xa1LHrVLJSCQ7nri3YbvjugyoWguKWwHoynzXICla5MrUjvS11OOOWExFE/YsILPfXw1bavJz7hhDPoDUDSsE91fvlpa79jzhOynTMe6c15FQBw+4e3fgJwoH4i8TObmHgDenpLqZB41FxOcLCBTMrJyTG7P8QNl6tqAwH3mcQTGVgtmb7MdGRtzXRkt4yfMOlrEDwAzTLkJLxyxBFjKwdPYyPdb/yQ4ZMIwb0HAPg27m8FEOQpoMXa7b0bmUA/R/DwJkkjUdvXk5L5jfmdSWO0+QA16MQ5SakT15rzth29500Afl/ABPbJ+0MZkp7y9DX/ZfO2f1mWVvlMWdray3rNT+Khnge+AAxn4InwkmHi3o0QQrYAMIwtO6W8oDelBhuXq6aIgUdCJKndBpECYExwMr2udXqvf/TRRyNuasWS9kTbw7pJop8P25r3/SlU/u6xflCvIwZpL2TZ+wVfEWhxcAqn2I7YH3InujeWvL2krb0N1wD8nl9GhFvK0iru0udzNjh9TPQrneg6W9rEp3578uqgF3a9vV4pT6/8ETOvR9d3fx3A95xiPyWsz92Jyce+xIRP/CqAoPdkCDkMFrW1tXsAMqwWkTSeUR8CcJ2r5mcE+imAKIZK3AGg9IvW/1zi8Xji7lvmbMw/pBFdzKB5gY/Uru5t0sshJupyEPdCipoLagA8G5TAWFKavvai4BK949xesF9N9F0GgzcA/bostdLgj1bSVOAixmM60XWKKv5Vllb5Slla5ary1IoVpWmV1Tu2tH7KjCcB+I8K7CPWcno7wTnfM18jpsdCJH3U0bQnpBuSCgAM3kDQ+fsTDzUDAQC4XNWP5eXlPdXe7r0JhAyA5sAYVaQFTC8qCu6vqakZUnGZljcVtgBoiboA4d9mTw/iwd0L0XxyoaKKCwBM0okFsVy3KmVVel9DAN3eePuu8tPWzmOf/F/4vXQJK0rTK/eWNBUGfrgTkmy37O5o9/a4iCARXRuP85gIIbqI/wiS1yxtXvxhJB2EVB6Twvdr6H83jIfDvay6wv5wUBSTmfacnCGzmqXnySef3Ot2197ndtWepfm8o8DacZpPOV7zece5XTVpbnd18VAzjn4hg3sQ0OCeC1n+4eJWIiwJkTTVS0rQhDYait6/7XPBNA/A590iIsbDZamVAUfB/Mb8zpKWwoWyy2mxt6X5/SDch0Rbqn6jtTeWblr4BRP0jpZt0qeEdZ1RAeDQ6MQ3Rrd1tEO3I6lqvADAHdE0Gi88Ho8GIMSm2vBHgtcKkGGIw1qwI2lbm7LBlmR0RydFhnexkeL7LFh//wvY2xY2BlpRU+EfS+esaYI0BvYAdXnU6pdrVZ/4baeqdc0VFQ47FF7aUvDRipQHztCEL7DrLQWClq+XNRe+BOCl0pRVM6Aoc4XEVBCPkoyvQGLjmDa82ddl7+5gf/rVOPeyLbeG1TXQW2U6susAzNelfTI7eeaJVvhRi5FEeXrF1cz0nP+ZJZ9dsmlRWCfYwFuBBcyhRo/fvG3bkFrNsrA4bPTOiox/9GYcgM5Adn722XoAOw2JkkKNPy0shiX3pP5uGgMBd3gSCDruayZgIA0NDT4QGXsRwhUZGTlzgkpZWAxDVFJuRperCQDsaUuweSKVMUy8VIFVMJ6hIFLYFDncwmL44ZzrVAH6sf+ZmB8LFzJIj8FAqqurWxn4oyEH44aMjJzTYWExjEnaNfEq9JyLZ1Ip/JUSOoL8VhTiFdBHBAEUIn7Y6ezdJ8bCYihDPW73IODPSz8o/Gdv+f0E/ehra2t3gOkPptrP2rp1+43mvBYWw4GV6SvHkMBeAnm6P1FHpw/p2JWbm3ukt1PbAhgia3ytKpQ2VANbW1jEAiWUcOPGjW2pqel7QYYLXUZLpjNPPjm5avPmzRFjnFpYjATCzitmz575OIC3jVK+QElI+lXIAhYWI5Be49Xac3JmKBo3wniOWiPQlS5X9frYqmZhEX96XZnyVFdvZwq6u0Jh4qcdDse5MdTLwmJIEHIOomdTc/PmlLT0KQToY80mAHRNWlrq8y0tLbtiqJ+FRVyJam9jVFLCIgBvmsRHMYmXMzJyZ4UqY2ExEoj6zgy73T5eqIkNhKBQ+LsJ8kqXy/V2yIIWFsOYqHfHPR7PPulTrgQCh979TALEyxkZ2d8fWNUsLOJPn29dysjInUVCewUICiCgMXD3yckz77EOWQ0eeXl5E7xebyAappRSut3uoEs3LfpHn/2r6urWbVMVOhegJlOSQsBdW7f+8y9DKa7WSKe93Xe+ZHrP/wEpf4tcyiJa+uWAWF1d3SpIzjWEle+GgQt9Gm/KyMoptBwcLYY7/f4B19bW7tE07zwwPR4ieSIxV2zZuv317OzstMPQz8IirhzWG97j8bS53dU3EbAApot4uuDzNIkPMjOzn7fOlFgMRwZkCORy1VQT5Fn60JKGNghXkuB3MzOzn3Y4HOdhgK9ktrCIFQM2R3C5XFtmJ886h4CF3HVtb3BbhB8yxJuZjuztmZlZd2ZkZAyZu0gsLEIRkze5w+GYzBDl6Bp6RTBC3gaiDQzeIJjfcLlcO3vPb6EnMzPnahA/pxO1uV01Ea8DsIiOmA517Dk5yYrGJQCy0R3FMQp2A9hK4C3M2EFEBwE6KEnug0b7WGFrj0WHgDgXzPq74y0DGUAGZS5gX7DgBKVTWwKibAyhOxBHKF+6XTWTI2eziIaI3rwDweampr0tLc1/njbtuxWjbKM/QFcM4BMHq/1vFYTnWpqbn4m3GiOFuK0m5eXlTejo8M2V4IsEcDEDp8RLlxHE12At3e12fx45q0U0DJnl1gULFhzToWkpQlIyE5Kp667EowGaAPBYAGMR8hYpCwIOMPCyIrB4RFz9MIT4fy9/yfbOhdfBAAAAAElFTkSuQmCC" + }, + "1c086528-58d5-f211-823c-356786e36140": { + "name": "Atos CardOS FIDO2", + "icon_light": "data:image/png;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAKAEGACtYuHw7fWlJAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAKAEGACtYuHw7fWlJAAAAAElFTkSuQmCC" + }, + "77010bd7-212a-4fc9-b236-d2ca5e9d4084": { + "name": "Feitian BioPass FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "d94a29d9-52dd-4247-9c2d-8b818b610389": { + "name": "VeriMark Guard Fingerprint Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "833b721a-ff5f-4d00-bb2e-bdda3ec01e29": { + "name": "Feitian ePass FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + } +} diff --git a/x/webauthnx/aaguid/passkey-aaguids.json b/x/webauthnx/aaguid/passkey-aaguids.json new file mode 100644 index 000000000000..8410d2e6c5b9 --- /dev/null +++ b/x/webauthnx/aaguid/passkey-aaguids.json @@ -0,0 +1,105 @@ +{ + "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": { + "name": "Google Password Manager", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=" + }, + "adce0002-35bc-c60a-648b-0b25f1f05503": { + "name": "Chrome on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==" + }, + "08987058-cadc-4b81-b6e1-30de50dcbe96": { + "name": "Windows Hello", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==" + }, + "9ddd1817-af5a-4672-a2b9-3e3dd95000a9": { + "name": "Windows Hello", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==" + }, + "6028b017-b1d4-4c02-b4b3-afcdafc96bb2": { + "name": "Windows Hello", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==" + }, + "dd4ec289-e01d-41c9-bb89-70fa845d4bf2": { + "name": "iCloud Keychain (Managed)", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iIzAwMCIvPjwvc3ZnPg==" + }, + "531126d6-e717-415c-9320-3d9aa6981239": { + "name": "Dashlane", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDMyMS4xMDRDMzUyLjczNiAzMTguMDg0IDM0OS43MTcgMzE1LjA2NCAzNDUuNDc3IDMxMy44NTRMMjk0LjY3NCAyOTUuMTI2QzI4Ni4xNTcgMjkxLjQ5NiAyNzYuNTI2IDI5NS43MjYgMjc2LjUyNiAzMDEuNzg1VjQzNy42NzRDMjc2LjUyNiA0NDAuNzA0IDI3OS41NDYgNDQ0LjMzMyAyODIuNTY2IDQ0NS41NDNMMzM0LjU4OSA0NjQuMjcxQzM0Mi40NTggNDY3LjI5MSAzNTIuNzM2IDQ2My4wNjEgMzUyLjczNiA0NTYuNDAzVjMyMS4xMDRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjU3LjQ3NCAzNS4zMjExQzI1Ny40NzQgMzIuMzAxMyAyNTQuNDU0IDI5LjI4MTUgMjUwLjIxNSAyOC4wNzE3TDE5OS40MTEgOS4zNDM0M0MxOTAuODk1IDUuNzEzOTkgMTgxLjI2NCA5Ljk0MzU4IDE4MS4yNjQgMTYuMDAyMlYxNTEuODkyQzE4MS4yNjQgMTU0LjkyMSAxODQuMjgzIDE1OC41NTEgMTg3LjMwMyAxNTkuNzZMMjM5LjMyNiAxNzguNDg5QzI0Ny4xOTUgMTgxLjUwOSAyNTcuNDc0IDE3Ny4yNzkgMjU3LjQ3NCAxNzAuNjJWMzUuMzIxMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDQ4IDE2OC42ODdDNDQ4IDE2NS42NjcgNDQ0Ljk4IDE2Mi42NDcgNDQwLjc0MSAxNjEuNDM3TDM4OS45MzcgMTQyLjcwOUMzODEuNDIxIDEzOS4wNzkgMzcxLjc5IDE0My4zMDkgMzcxLjc5IDE0OS4zNjhWMzYxLjQ2NkMzNzEuNzkgMzY0LjQ5NSAzNzQuODEgMzY4LjEyNSAzNzcuODI5IDM2OS4zMzVMNDI5Ljg1MiAzODguMDYzQzQzNy43MjEgMzkxLjA4MyA0NDggMzg2Ljg1MyA0NDggMzgwLjE5NFYxNjguNjg3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE2Mi4yMSAzNS4zMzA2QzE2Mi4yMSAzMi4zMTA4IDE1OS4xOSAyOS4yODE1IDE1NC45NTEgMjguMDcxN0wxMDQuMTQ4IDkuMzQzNDNDOTUuNjc4NyA1LjcxMzk5IDg2IDkuOTQzNTggODYgMTYuMDAyMlY0NzUuNzg5Qzg2IDQ3OC44MDggODkuMDE5OCA0ODIuNDM4IDkyLjA0OTIgNDgzLjY0OEwxNDQuMDYzIDUwMi4zNzZDMTUxLjkzMSA1MDUuMzk2IDE2Mi4yMSA1MDEuMTY2IDE2Mi4yMSA0OTQuNTA3VjM1LjMzMDZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTM1Mi43MzYgMzIxLjEwM0MzNTIuNzM2IDMxOC4wODQgMzQ5LjcxNyAzMTUuMDY0IDM0NS40NzcgMzEzLjg1NEwyOTQuNjc0IDI5NS4xMjZDMjg2LjE1NyAyOTEuNDk2IDI3Ni41MjYgMjk1LjcyNiAyNzYuNTI2IDMwMS43ODVWNDM3LjY3NEMyNzYuNTI2IDQ0MC43MDQgMjc5LjU0NiA0NDQuMzMzIDI4Mi41NjYgNDQ1LjU0M0wzMzQuNTg5IDQ2NC4yNzFDMzQyLjQ1OCA0NjcuMjkxIDM1Mi43MzYgNDYzLjA2MSAzNTIuNzM2IDQ1Ni40MDNWMzIxLjEwM1oiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTI1Ny40NzQgMzUuMzIxMUMyNTcuNDc0IDMyLjMwMTMgMjU0LjQ1NCAyOS4yODE1IDI1MC4yMTUgMjguMDcxN0wxOTkuNDExIDkuMzQzNDNDMTkwLjg5NSA1LjcxMzk5IDE4MS4yNjQgOS45NDM1OCAxODEuMjY0IDE2LjAwMjJWMTUxLjg5MkMxODEuMjY0IDE1NC45MjEgMTg0LjI4MyAxNTguNTUxIDE4Ny4zMDMgMTU5Ljc2TDIzOS4zMjYgMTc4LjQ4OUMyNDcuMTk1IDE4MS41MDggMjU3LjQ3NCAxNzcuMjc5IDI1Ny40NzQgMTcwLjYyVjM1LjMyMTFaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik00NDggMTY4LjY4N0M0NDggMTY1LjY2NyA0NDQuOTggMTYyLjY0NyA0NDAuNzQxIDE2MS40MzdMMzg5LjkzNyAxNDIuNzA5QzM4MS40MjEgMTM5LjA3OSAzNzEuNzkgMTQzLjMwOSAzNzEuNzkgMTQ5LjM2OFYzNjEuNDY2QzM3MS43OSAzNjQuNDk1IDM3NC44MSAzNjguMTI1IDM3Ny44MjkgMzY5LjMzNUw0MjkuODUyIDM4OC4wNjNDNDM3LjcyMSAzOTEuMDgzIDQ0OCAzODYuODUzIDQ0OCAzODAuMTk0VjE2OC42ODdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0xNjIuMjEgMzUuMzMwNkMxNjIuMjEgMzIuMzEwOCAxNTkuMTkgMjkuMjgxNSAxNTQuOTUxIDI4LjA3MTdMMTA0LjE0OCA5LjM0MzQzQzk1LjY3ODcgNS43MTM5OSA4NiA5Ljk0MzU4IDg2IDE2LjAwMjJWNDc1Ljc4OUM4NiA0NzguODA4IDg5LjAxOTggNDgyLjQzOCA5Mi4wNDkyIDQ4My42NDhMMTQ0LjA2MyA1MDIuMzc2QzE1MS45MzEgNTA1LjM5NiAxNjIuMjEgNTAxLjE2NiAxNjIuMjEgNDk0LjUwN1YzNS4zMzA2WiIgZmlsbD0iIzA5MzYzRiIvPgo8L3N2Zz4K" + }, + "bada5566-a7aa-401f-bd96-45619a55120d": { + "name": "1Password", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjI1NyAxMjAuNDE3QzIzOS4yNTcgNTQuNDE5MyAxODUuNzU1IDAuOTE2NTA0IDExOS43NTcgMC45MTY1MDRDNTMuNzYwMSAwLjkxNjUwNCAwLjI1NzMyNCA1NC40MTkzIDAuMjU3MzI0IDEyMC40MTdDMC4yNTczMjQgMTg2LjQxNyA1My43NjAxIDIzOS45MTcgMTE5Ljc1NyAyMzkuOTE3QzE4NS43NTUgMjM5LjkxNyAyMzkuMjU3IDE4Ni40MTcgMjM5LjI1NyAxMjAuNDE3Wk05OC4wMDY5IDU0LjAyNzZDOTcuMDY3NCA1NS44NzE0IDk3LjA2NzQgNTguMjg1MSA5Ny4wNjc0IDYzLjExMjZWOTAuNDcyOUM5Ny4wNjc0IDkxLjY3ODggOTcuMDY3NCA5Mi4yODE3IDk3LjIxOTYgOTIuODM5MkM5Ny4zNTQ1IDkzLjMzMzEgOTcuNTc2MyA5My43OTkgOTcuODc0NiA5NC4yMTVDOTguMjExMyA5NC42ODQ3IDk4LjY3OTIgOTUuMDY0OCA5OS42MTUyIDk1LjgyNTFMMTA2LjUzNiAxMDEuNDQ3QzEwNy42NjQgMTAyLjM2NCAxMDguMjI4IDEwMi44MjIgMTA4LjQzMyAxMDMuMzc0QzEwOC42MTMgMTAzLjg1NyAxMDguNjEzIDEwNC4zOSAxMDguNDMzIDEwNC44NzNDMTA4LjIyOCAxMDUuNDI1IDEwNy42NjQgMTA1Ljg4MyAxMDYuNTM2IDEwNi44TDk5LjYxNTIgMTEyLjQyMkM5OC42NzkzIDExMy4xODIgOTguMjExMyAxMTMuNTYyIDk3Ljg3NDYgMTE0LjAzMkM5Ny41NzYzIDExNC40NDggOTcuMzU0NSAxMTQuOTE0IDk3LjIxOTYgMTE1LjQwOEM5Ny4wNjc0IDExNS45NjUgOTcuMDY3NCAxMTYuNTY4IDk3LjA2NzQgMTE3Ljc3NFYxNzcuNzE5Qzk3LjA2NzQgMTgyLjU0NyA5Ny4wNjc0IDE4NC45NjEgOTguMDA2OSAxODYuODA1Qzk4LjgzMzMgMTg4LjQyNiAxMDAuMTUyIDE4OS43NDUgMTAxLjc3NCAxOTAuNTcxQzEwMy42MTggMTkxLjUxMSAxMDYuMDMxIDE5MS41MTEgMTEwLjg1OSAxOTEuNTExSDEyOC42NTZDMTMzLjQ4MyAxOTEuNTExIDEzNS44OTcgMTkxLjUxMSAxMzcuNzQxIDE5MC41NzFDMTM5LjM2MyAxODkuNzQ1IDE0MC42ODEgMTg4LjQyNiAxNDEuNTA4IDE4Ni44MDVDMTQyLjQ0NyAxODQuOTYxIDE0Mi40NDcgMTgyLjU0NyAxNDIuNDQ3IDE3Ny43MTlWMTUwLjM1OUMxNDIuNDQ3IDE0OS4xNTMgMTQyLjQ0NyAxNDguNTUgMTQyLjI5NSAxNDcuOTkzQzE0Mi4xNiAxNDcuNDk5IDE0MS45MzggMTQ3LjAzMyAxNDEuNjQgMTQ2LjYxN0MxNDEuMzAzIDE0Ni4xNDcgMTQwLjgzNSAxNDUuNzY3IDEzOS44OTkgMTQ1LjAwN0wxMzIuOTc4IDEzOS4zODVDMTMxLjg1IDEzOC40NjggMTMxLjI4NiAxMzguMDEgMTMxLjA4MiAxMzcuNDU5QzEzMC45MDIgMTM2Ljk3NSAxMzAuOTAyIDEzNi40NDMgMTMxLjA4MiAxMzUuOTU5QzEzMS4yODYgMTM1LjQwNyAxMzEuODUgMTM0Ljk0OSAxMzIuOTc4IDEzNC4wMzNMMTM5Ljg5OSAxMjguNDFDMTQwLjgzNSAxMjcuNjUgMTQxLjMwMyAxMjcuMjcgMTQxLjY0IDEyNi44QzE0MS45MzggMTI2LjM4NCAxNDIuMTYgMTI1LjkxOCAxNDIuMjk1IDEyNS40MjRDMTQyLjQ0NyAxMjQuODY3IDE0Mi40NDcgMTI0LjI2NCAxNDIuNDQ3IDEyMy4wNThWNjMuMTEyNkMxNDIuNDQ3IDU4LjI4NTEgMTQyLjQ0NyA1NS44NzE0IDE0MS41MDggNTQuMDI3NkMxNDAuNjgxIDUyLjQwNTcgMTM5LjM2MyA1MS4wODcgMTM3Ljc0MSA1MC4yNjA2QzEzNS44OTcgNDkuMzIxMSAxMzMuNDgzIDQ5LjMyMTEgMTI4LjY1NiA0OS4zMjExSDExMC44NTlDMTA2LjAzMSA0OS4zMjExIDEwMy42MTggNDkuMzIxMSAxMDEuNzc0IDUwLjI2MDZDMTAwLjE1MiA1MS4wODcgOTguODMzMyA1Mi40MDU3IDk4LjAwNjkgNTQuMDI3NloiIGZpbGw9IiNGRkZFRkIiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjExNiAxMjAuNDE3QzIzOS4xMTYgNTQuNDE5MyAxODUuNjEzIDAuOTE2NTA0IDExOS42MTYgMC45MTY1MDRDNTMuNjE5IDAuOTE2NTA0IDAuMTE2MjExIDU0LjQxOTMgMC4xMTYyMTEgMTIwLjQxN0MwLjExNjIxMSAxODYuNDE3IDUzLjYxOSAyMzkuOTE3IDExOS42MTYgMjM5LjkxN0MxODUuNjEzIDIzOS45MTcgMjM5LjExNiAxODYuNDE3IDIzOS4xMTYgMTIwLjQxN1pNOTcuODY1OCA1NC4wMjc2Qzk2LjkyNjMgNTUuODcxNCA5Ni45MjYzIDU4LjI4NTEgOTYuOTI2MyA2My4xMTI2VjkwLjQ3MjlDOTYuOTI2MyA5MS42Nzg4IDk2LjkyNjMgOTIuMjgxNyA5Ny4wNzg1IDkyLjgzOTJDOTcuMjEzNCA5My4zMzMxIDk3LjQzNTIgOTMuNzk5IDk3LjczMzUgOTQuMjE1Qzk4LjA3MDIgOTQuNjg0NyA5OC41MzgxIDk1LjA2NDggOTkuNDc0MSA5NS44MjUxTDEwNi4zOTUgMTAxLjQ0N0MxMDcuNTIzIDEwMi4zNjQgMTA4LjA4NyAxMDIuODIyIDEwOC4yOTIgMTAzLjM3NEMxMDguNDcxIDEwMy44NTcgMTA4LjQ3MSAxMDQuMzkgMTA4LjI5MiAxMDQuODczQzEwOC4wODcgMTA1LjQyNSAxMDcuNTIzIDEwNS44ODMgMTA2LjM5NSAxMDYuOEw5OS40NzQxIDExMi40MjJDOTguNTM4MiAxMTMuMTgyIDk4LjA3MDIgMTEzLjU2MiA5Ny43MzM1IDExNC4wMzJDOTcuNDM1MiAxMTQuNDQ4IDk3LjIxMzQgMTE0LjkxNCA5Ny4wNzg1IDExNS40MDhDOTYuOTI2MyAxMTUuOTY1IDk2LjkyNjMgMTE2LjU2OCA5Ni45MjYzIDExNy43NzRWMTc3LjcxOUM5Ni45MjYzIDE4Mi41NDcgOTYuOTI2MyAxODQuOTYxIDk3Ljg2NTggMTg2LjgwNUM5OC42OTIyIDE4OC40MjYgMTAwLjAxMSAxODkuNzQ1IDEwMS42MzMgMTkwLjU3MUMxMDMuNDc3IDE5MS41MTEgMTA1Ljg5IDE5MS41MTEgMTEwLjcxOCAxOTEuNTExSDEyOC41MTVDMTMzLjM0MiAxOTEuNTExIDEzNS43NTYgMTkxLjUxMSAxMzcuNiAxOTAuNTcxQzEzOS4yMjEgMTg5Ljc0NSAxNDAuNTQgMTg4LjQyNiAxNDEuMzY3IDE4Ni44MDVDMTQyLjMwNiAxODQuOTYxIDE0Mi4zMDYgMTgyLjU0NyAxNDIuMzA2IDE3Ny43MTlWMTUwLjM1OUMxNDIuMzA2IDE0OS4xNTMgMTQyLjMwNiAxNDguNTUgMTQyLjE1NCAxNDcuOTkzQzE0Mi4wMTkgMTQ3LjQ5OSAxNDEuNzk3IDE0Ny4wMzMgMTQxLjQ5OSAxNDYuNjE3QzE0MS4xNjIgMTQ2LjE0NyAxNDAuNjk0IDE0NS43NjcgMTM5Ljc1OCAxNDUuMDA3TDEzMi44MzcgMTM5LjM4NUMxMzEuNzA5IDEzOC40NjggMTMxLjE0NSAxMzguMDEgMTMwLjk0IDEzNy40NTlDMTMwLjc2MSAxMzYuOTc1IDEzMC43NjEgMTM2LjQ0MyAxMzAuOTQgMTM1Ljk1OUMxMzEuMTQ1IDEzNS40MDcgMTMxLjcwOSAxMzQuOTQ5IDEzMi44MzcgMTM0LjAzM0wxMzkuNzU4IDEyOC40MUMxNDAuNjk0IDEyNy42NSAxNDEuMTYyIDEyNy4yNyAxNDEuNDk5IDEyNi44QzE0MS43OTcgMTI2LjM4NCAxNDIuMDE5IDEyNS45MTggMTQyLjE1NCAxMjUuNDI0QzE0Mi4zMDYgMTI0Ljg2NyAxNDIuMzA2IDEyNC4yNjQgMTQyLjMwNiAxMjMuMDU4VjYzLjExMjZDMTQyLjMwNiA1OC4yODUxIDE0Mi4zMDYgNTUuODcxNCAxNDEuMzY3IDU0LjAyNzZDMTQwLjU0IDUyLjQwNTcgMTM5LjIyMSA1MS4wODcgMTM3LjYgNTAuMjYwNkMxMzUuNzU2IDQ5LjMyMTEgMTMzLjM0MiA0OS4zMjExIDEyOC41MTUgNDkuMzIxMUgxMTAuNzE4QzEwNS44OSA0OS4zMjExIDEwMy40NzcgNDkuMzIxMSAxMDEuNjMzIDUwLjI2MDZDMTAwLjAxMSA1MS4wODcgOTguNjkyMiA1Mi40MDU3IDk3Ljg2NTggNTQuMDI3NloiIGZpbGw9IiMxQTI4NUYiLz4KPC9zdmc+Cg==" + }, + "b84e4048-15dc-4dd0-8640-f4f60813c8af": { + "name": "NordPass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSIjMENBQUFCIi8+Cjwvc3ZnPgo=" + }, + "0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": { + "name": "Keeper", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K" + }, + "f3809540-7f14-49c1-a8b3-8f813b225541": { + "name": "Enpass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0iIzBEMzM4RiIvPgo8L3N2Zz4K" + }, + "b5397666-4885-aa6b-cebf-e52262a439a2": { + "name": "Chromium Browser" + }, + "771b48fd-d3d4-4f74-9232-fc157ab0507a": { + "name": "Edge on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=" + }, + "39a5647e-1853-446c-a1f6-a79bae9f5bc7": { + "name": "IDmelon", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2YxNWI1Yzt9LmNscy0ye2ZpbGw6IzkyMWIxZDt9LmNscy0ze2ZpbGw6I2VlMzAyNTt9LmNscy00e2ZpbGw6I2JiMjAyNjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzQxLjg3LDEwMC4ybC00LjI5LTEuNjRjLTMyLjMxLTExLjgxLTY1LjM2LTEzLjI3LTc2LjkyLTEzLjRsLS44OSwwSDEyMC4xMkEyMy40MywyMy40MywwLDAsMCwxMTEuNiw4N2MtLjQxLjIxLS44MS40Mi0xLjE3LjY0bC0xLjg1LDEuNzYsMTMzLjM1LDY1LjgsMTAzLjM4LTUyLjg5WiIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjI5NC41OCIgeTE9IjEzNy4wNyIgeDI9IjI5Ni45OSIgeTI9IjEzOC4yNyIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjIzOS41MyIgeTE9IjE1Mi4xMSIgeDI9IjI0MS45MyIgeTI9IjE1My4zMSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTEwNi43NCw5MXEtMi42Miw0LjItMi4zNSwxMS4yNnQuMjYsMTMuMzdWNDIzLjIxcTAsNS43Ni0uMjYsMTQuNDF0MS4zMSwxMi44NGExNC41NSwxNC41NSwwLDAsMCwxLjE0LDIuMTlsMTM2LTI5OS41NkwxMTAuNDMsODcuNjVBMTEuMjQsMTEuMjQsMCwwLDAsMTA2Ljc0LDkxWiIvPjxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTM2MS44NiwxMTEuNTNjLTIuMzItMS41NS00LjctMy4xLTcuMTMtNC42OGE5My45Miw5My45MiwwLDAsMC0xMi02LjMyYy0uMjctLjExLS41NS0uMjMtLjgzLS4zM2wtOTksNTIuODlMMzg3LjYzLDQwMi4zMUExNjQuMDcsMTY0LjA3LDAsMCwwLDM5NywzODguMTJxMjkuODItNTEuMjEsMjkuODItMTI1YTI4NC44MywyODQuODMsMCwwLDAtNy4wOC02MS4yNSwxNjQuMTYsMTY0LjE2LDAsMCwwLTI2LjUzLTU5Ljc1LDEzNC45LDEzNC45LDAsMCwwLTkuMDUtMTEuMzhBMTUzLjIsMTUzLjIsMCwwLDAsMzYxLjg2LDExMS41M1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMDYuNjUsNDUyLjM0YTEwLjA3LDEwLjA3LDAsMCwwLDcuNjksNS4xOWwxLjc0LjJoMTU2YzUwLjI3LDAsODguNjQtMTguNjksMTE1LjUyLTU1LjQyTDI0Mi44OSwxNTMuMDlaIi8+PC9zdmc+", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2YxNWI1Yzt9LmNscy0ye2ZpbGw6IzkyMWIxZDt9LmNscy0ze2ZpbGw6I2VlMzAyNTt9LmNscy00e2ZpbGw6I2JiMjAyNjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzQxLjg3LDEwMC4ybC00LjI5LTEuNjRjLTMyLjMxLTExLjgxLTY1LjM2LTEzLjI3LTc2LjkyLTEzLjRsLS44OSwwSDEyMC4xMkEyMy40MywyMy40MywwLDAsMCwxMTEuNiw4N2MtLjQxLjIxLS44MS40Mi0xLjE3LjY0bC0xLjg1LDEuNzYsMTMzLjM1LDY1LjgsMTAzLjM4LTUyLjg5WiIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjI5NC41OCIgeTE9IjEzNy4wNyIgeDI9IjI5Ni45OSIgeTI9IjEzOC4yNyIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjIzOS41MyIgeTE9IjE1Mi4xMSIgeDI9IjI0MS45MyIgeTI9IjE1My4zMSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTEwNi43NCw5MXEtMi42Miw0LjItMi4zNSwxMS4yNnQuMjYsMTMuMzdWNDIzLjIxcTAsNS43Ni0uMjYsMTQuNDF0MS4zMSwxMi44NGExNC41NSwxNC41NSwwLDAsMCwxLjE0LDIuMTlsMTM2LTI5OS41NkwxMTAuNDMsODcuNjVBMTEuMjQsMTEuMjQsMCwwLDAsMTA2Ljc0LDkxWiIvPjxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTM2MS44NiwxMTEuNTNjLTIuMzItMS41NS00LjctMy4xLTcuMTMtNC42OGE5My45Miw5My45MiwwLDAsMC0xMi02LjMyYy0uMjctLjExLS41NS0uMjMtLjgzLS4zM2wtOTksNTIuODlMMzg3LjYzLDQwMi4zMUExNjQuMDcsMTY0LjA3LDAsMCwwLDM5NywzODguMTJxMjkuODItNTEuMjEsMjkuODItMTI1YTI4NC44MywyODQuODMsMCwwLDAtNy4wOC02MS4yNSwxNjQuMTYsMTY0LjE2LDAsMCwwLTI2LjUzLTU5Ljc1LDEzNC45LDEzNC45LDAsMCwwLTkuMDUtMTEuMzhBMTUzLjIsMTUzLjIsMCwwLDAsMzYxLjg2LDExMS41M1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMDYuNjUsNDUyLjM0YTEwLjA3LDEwLjA3LDAsMCwwLDcuNjksNS4xOWwxLjc0LjJoMTU2YzUwLjI3LDAsODguNjQtMTguNjksMTE1LjUyLTU1LjQyTDI0Mi44OSwxNTMuMDlaIi8+PC9zdmc+" + }, + "d548826e-79b4-db40-a3d8-11116f7e8349": { + "name": "Bitwarden", + "icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K" + }, + "fbfc3007-154e-4ecc-8c0b-6e020557d7bd": { + "name": "iCloud Keychain", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iIzAwMCIvPjwvc3ZnPg==" + }, + "53414d53-554e-4700-0000-000000000000": { + "name": "Samsung Pass", + "icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgd2lkdGg9IjUycHgiIGhlaWdodD0iNTJweCIgdmlld0JveD0iMCAwIDUyLjAgNTIuMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PGNsaXBQYXRoIGlkPSJpMCI+PHBhdGggZD0iTTM2MCwwIEwzNjAsODAwIEwwLDgwMCBMMCwwIEwzNjAsMCBaIj48L3BhdGg+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkxIj48cGF0aCBkPSJNMjYsMCBDMzMuOTkxMDI3OCwwIDQxLjEzOTU4MzMsMC45NzUgNDUuOTA4Nzc3OCw1Ljc3Nzc3Nzc4IEM0OS43MTAxOTQ0LDkuNjA1OTE2NjcgNTIsMTUuODY1MDU1NiA1MiwyNiBDNTIsMzYuMTM0OTQ0NCA0OS43MDk4MzMzLDQyLjM5NDQ0NDQgNDUuOTA4MDU1Niw0Ni4yMjI1ODMzIEM0MS4xMzg4NjExLDUxLjAyNDYzODkgMzMuOTkwMzA1Niw1MiAyNiw1MiBDMTguMDA4OTcyMiw1MiAxMC44NjA3Nzc4LDUxLjAyNDYzODkgNi4wOTE1ODMzMyw0Ni4yMjI1ODMzIEMyLjI5MDE2NjY3LDQyLjM5NDQ0NDQgMCwzNi4xMzQ5NDQ0IDAsMjYgQzAsMTUuODY1MDU1NiAyLjI4OTgwNTU2LDkuNjA1NTU1NTYgNi4wOTA4NjExMSw1Ljc3Nzc3Nzc4IEMxMC44NjAwNTU2LDAuOTc1IDE4LjAwODYxMTEsMCAyNiwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxsaW5lYXJHcmFkaWVudCBpZD0iaTIiIHgxPSIyNnB4IiB5MT0iNTJweCIgeDI9IjI2cHgiIHkyPSIwLjE5NTkyMTE0OHB4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzI5MjlCMiIgb2Zmc2V0PSIwJSI+PC9zdG9wPjxzdG9wIHN0b3AtY29sb3I9IiMxQTQwQ0MiIG9mZnNldD0iMTAwJSI+PC9zdG9wPjwvbGluZWFyR3JhZGllbnQ+PGNsaXBQYXRoIGlkPSJpMyI+PHBhdGggZD0iTTM3LjE5NDQ0NDQsMCBMMzcuMTk0NDQ0NCw1LjcyMjIyMjIyIEwwLDUuNzIyMjIyMjIgTDAsMCBMMzcuMTk0NDQ0NCwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTQiPjxwYXRoIGQ9Ik0xLjg4MzQyMjEzLDAgQzIuNjIwNzU5ODcsMCAzLjY0MzU2NDAyLDAuMTgxNjEwODcxIDMuNjQzNTY0MDIsMS4zMjExMTgxOSBMMy42NDM1NjQwMiwxLjY4OTExNTYyIEwyLjM0Mjc5MzM5LDEuNjg5MTE1NjIgTDIuMzQyNzkzMzksMS4zNjQ5MDY1OSBDMi4zNDI3OTMzOSwxLjA3OTU3NTczIDIuMTYzMTk2NjQsMC44ODkxNTMzNzEgMS44NTgxNzY4LDAuODg5MTUzMzcxIEMxLjUyOTMxNzg4LDAuODg5MTUzMzcxIDEuNDE2NDgzOTgsMS4wNzUyNzA4OCAxLjM4MDg1OTI3LDEuMjQyMTUxMDkgQzEuMzY2Nzk2ODgsMS4zMDAyNjY1NyAxLjM2MDkwNDA4LDEuNDExNzIxODQgMS4zODYxNDk0MSwxLjUxNzI1NzkzIEMxLjUzNDYwODAyLDIuMTMwNDk3MyAzLjUxMjQ0OTAyLDIuNDU2MTE4ODcgMy43MzI1NTg4MywzLjU1NTUzNzI3IEMzLjc1MzcxOTM3LDMuNjY3MDU5OCAzLjgwMDc5NDg4LDMuOTYxMTM0ODggMy43MzgwNDk4Niw0LjQxMzAwOTYzIEMzLjYxMTgyMzIxLDUuMjg5NjUyMDMgMi44MzgwNTcyLDUuNjEyMTc5NDkgMS44OTU4MTA0Myw1LjYxMjE3OTQ5IEMwLjkxNTcyOTEzNiw1LjYxMjE3OTQ5IDAsNS4yNTk5ODg5MiAwLDQuMDg4ODAwNiBMMCwzLjY4ODI0NzczIEwxLjM5OTQwODIzLDMuNjg4MjQ3NzMgTDEuNDAxMjE2MjUsNC4xOTIzMTg3OSBDMS40MDEyMTYyNSw0LjQ3ODY1ODYgMS41OTYwODA3Myw0LjY2OTI4Mjc1IDEuOTIxODU5MzIsNC42NjkyODI3NSBDMi4yNzA3NDA0LDQuNjY5MjgyNzUgMi4zODgzOTU2OSw0LjQ5MTUwNTg5IDIuNDMxMTg1NTIsNC4zMTU0MTA2MSBDMi40NTYyMjk5Niw0LjIxNjM5OTA1IDIuNDcxNDk3NjksNC4wNTQ2MzA4NSAyLjQyMTAwNzAzLDMuOTI4NjQ2NzEgQzIuMTUxMzQ0MDYsMy4yNTA5NjkxMSAwLjI5NzkyMTY3NywyLjk0MTI4ODk1IDAuMDcyMTE5OTQ3MywxLjg3MDMyMjkyIEMwLjAxNzM0MzYwODUsMS42MDU2NDE4OSAwLjAyMzgzOTA5MTIsMS4zOTkwNzYzNCAwLjA2MTc0MDU2NzcsMS4xNjQyNjAyMSBDMC4yMDAyMjE1ODEsMC4zMDg4NzMwMDcgMC45NTcxMTI3MjcsMCAxLjg4MzQyMjEzLDAgWiBNMTguODQxMTE4NiwwLjAyOTY2MzEwODkgQzE5LjU3MDQ4NzcsMC4wMjk2NjMxMDg5IDIwLjU3Nzg5MDEsMC4yMDY5NjkxMjkgMjAuNTc3ODkwMSwxLjMzNTY0NzA2IEwyMC41Nzc4OTAxLDEuNzAwNzUyMTcgTDE5LjI5MjE4NjQsMS43MDA3NTIxNyBMMTkuMjkyMTg2NCwxLjM4MDQ0NDQxIEMxOS4yOTIxODY0LDEuMDk3NDAwNSAxOS4xMTU2MDMsMC45MDg3OTQyNSAxOC44MTQ0MDAxLDAuOTA4Nzk0MjUgQzE4LjQ5MTIzMzEsMC45MDg3OTQyNSAxOC4zNzkwNjg4LDEuMDkxNjE1ODYgMTguMzQxNzcsMS4yNTkzNzA0OSBDMTguMzI5NzgzNSwxLjMxNjgxMzM0IDE4LjMyMzA4NzEsMS40MjY2NTQyOCAxOC4zNDYyNTY2LDEuNTMwMTcyNDggQzE4LjQ5NDMxMzQsMi4xMzY0MTY0NyAyMC40NTAzOTEyLDIuNDYzMTE0MjUgMjAuNjY2MDE0NCwzLjU0OTIxNDUyIEMyMC42ODk2NTI2LDMuNjU5MDU1NDcgMjAuNzM0MDQ5NiwzLjk1MDcwOTA3IDIwLjY3NTE4ODUsNC4zOTg0MTM1IEMyMC41NTE3NzQzLDUuMjY1MTAwOTMgMTkuNzgyNDk0OSw1LjU4NDgwMzMzIDE4Ljg1MTA5NjIsNS41ODQ4MDMzMyBDMTcuODc4NTgxOCw1LjU4NDgwMzMzIDE2Ljk3NTY0MjgsNS4yMzU0Mzc4MyAxNi45NzU2NDI4LDQuMDc3NTAwMzcgTDE2Ljk3NTY0MjgsMy42Nzc4ODkxOSBMMTguMzU5NTE1NCwzLjY3Nzg4OTE5IEwxOC4zNTk5MTcyLDQuMTgwNjE0OTggQzE4LjM1OTkxNzIsNC40NjMwNTM1MiAxOC41NTUxODM0LDQuNjUwNDQ5MDMgMTguODc5NzU2Nyw0LjY1MDQ0OTAzIEMxOS4yMjU3NTgzLDQuNjUwNDQ5MDMgMTkuMzQyNDc2MSw0LjQ3NTE2MDkxIDE5LjM4MjM4NjUsNC4zMDA4ODE3NCBDMTkuNDA2MzU5NSw0LjIwNTU2OTY2IDE5LjQxOTgxOTIsNC4wNDM4MDE0NiAxOS4zNzI4MTA3LDMuOTE2OTQyOSBDMTkuMTA2NDI4OSwzLjI0NzI2OTYzIDE3LjI3MTE1MzcsMi45NDAwNzgyMSAxNy4wNDc3NjI3LDEuODgxMTUyMyBDMTYuOTkwMTA2OSwxLjYxOTM2MzYgMTYuOTk5MDgwMSwxLjQxNDIxMDU4IDE3LjAzNTMwNzQsMS4xODMwOTM5MyBDMTcuMTcyMzE1MywwLjMzMzgyNzY4NiAxNy45MjI1MSwwLjAyOTY2MzEwODkgMTguODQxMTE4NiwwLjAyOTY2MzEwODkgWiBNMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBMMjMuMjM2MjI5MSw0LjExMTM3MzY2IEMyMy4yMzcyMDYzLDQuMTU3OTg0NjIgMjMuMjQwODgxOCw0LjIwNDA2NzQ1IDIzLjI0OTY3NjQsNC4yNDExNTE5NCBDMjMuMjc1MTIyNiw0LjM3MTIzOTEzIDIzLjM4Nzc1NTYsNC42MjE1OTMwOCAyMy43NDY5NDkxLDQuNjIxNTkzMDggQzI0LjExMDgzMDEsNC42MjE1OTMwOCAyNC4yMjA1ODM2LDQuMzcxMjM5MTMgMjQuMjQ4MTA1Nyw0LjI0MTE1MTk0IEMyNC4yNTk2OTA1LDQuMTg1NTI1MiAyNC4yNjE3NjYzLDQuMTA5NjUyMjIgMjQuMjU5NjkwNSw0LjA0MjExOTg4IEwyNC4yNTk2OTA1LDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDMuOTIxODUzMTIgQzI1LjU4NDMwMDIsNC4wMTg2NDQ5OSAyNS41NzQ3MjQ0LDQuMjE2Mzk5MDUgMjUuNTY3NDI1Myw0LjI2ODE5MTc4IEMyNS40NzQ3NDc1LDUuMjQ2NjcwNzkgMjQuNzA3NDc3LDUuNTY0MzU1MjkgMjMuNzQ2OTQ5MSw1LjU2NDM1NTI5IEMyMi43ODgyOTYyLDUuNTY0MzU1MjkgMjIuMDIwNTU3LDUuMjQ2NjcwNzkgMjEuOTI5NTUzMiw0LjI2ODE5MTc4IEMyMS45MjM4NjEzLDQuMjE2Mzk5MDUgMjEuOTE2Mjk0NCw0LjAxODY0NDk5IDIxLjkxNzk2ODUsMy45MjE4NTMxMiBMMjEuOTE3OTY4NSwwLjE2Njg4MDIxMSBMMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBaIE0zNC42Mjk3NjIxLDAuMDI1OTYzNjI4MiBDMzUuNTUzOTk1NiwwLjAyNTk2MzYyODIgMzYuMzYwMDM4MiwwLjMzNjg1NDUzNCAzNi40NTg3NDI3LDEuMzE5NTAzODcgQzM2LjQ2NTU3MywxLjM5MTE5NjkyIDM2LjQ2ODM4MTQsMS40NjUxMTM3OCAzNi40NjkzNjA3LDEuNTI2MTgwMjIgTDM2LjQ2OTAwNCwxLjY1NTc1NDAyIEwzNi40Njg3MjAzLDEuNjY2MTc4ODQgTDM2LjQ2ODcyMDMsMS44MzgwMzY1NCBMMzUuMTU1MzYwNSwxLjgzODAzNjU0IEwzNS4xNTUxNSwxLjUzMzU1NTU2IEMzNS4xNTQ0NDcxLDEuNDk4NzMzNjIgMzUuMTUxNDksMS40MDU2NDEyMyAzNS4xMzk0OTAxLDEuMzQ1MjY1NzEgQzM1LjExNjY1NTUsMS4yMzEzMjE3IDM1LjAxNzA4MDQsMC45NjYwMzUzMDYgMzQuNjE4MTc3NCwwLjk2NjAzNTMwNiBDMzQuMjM4MTU4MiwwLjk2NjAzNTMwNiAzNC4xMjU1OTIxLDEuMjE3NTk5OTkgMzQuMDk5Njc3MiwxLjM0NTI2NTcxIEMzNC4wODE3OTc4LDEuNDE1NDIxMzIgMzQuMDc2MTA1OSwxLjUwODExMDEyIDM0LjA3NjEwNTksMS41OTI3OTQ2IEwzNC4wNzYxMDU5LDMuOTg2ODk2NzIgQzM0LjA3NjEwNTksNC4wNTQ2MzA4NSAzNC4wODAxOTA3LDQuMTI3NjExNTEgMzQuMDg5NzY2NSw0LjE4NjEzMDU3IEMzNC4xMTI1MzQyLDQuMzI3NTE4IDM0LjI0Mzg1MDEsNC41NjgyNTMzIDM0LjYyMDk4OTksNC41NjgyNTMzIEMzNS4wMDA0MDY0LDQuNTY4MjUzMyAzNS4xMzQxMzMsNC4zMjc1MTggMzUuMTU1MzYwNSw0LjE4NjEzMDU3IEMzNS4xNjY5NDUyLDQuMTI3NjExNTEgMzUuMTcwNjI4Miw0LjA1NDYzMDg1IDM1LjE2ODk1NDEsMy45ODY4OTY3MiBMMzUuMTY4OTU0MSwzLjIyODkwNjc2IEwzNC42MzQ0NDk2LDMuMjI4OTA2NzYgTDM0LjYzNDQ0OTYsMi40NjQ5MzAzNiBMMzYuNDc5MTY2NywyLjQ2NDkzMDM2IEwzNi40NzkxNjY3LDMuODY4NTEzMzQgQzM2LjQ3NzA5MDgsMy45NjQ0MzA3OCAzNi40NzU0ODM3LDQuMDM4Njg5NDUgMzYuNDYwMDE1LDQuMjEzOTc3NTcgQzM2LjM3MjgyODIsNS4xNjk1ODcwNyAzNS41NTM5OTU2LDUuNTA4MzI0OTcgMzQuNjI2MDc5MSw1LjUwODMyNDk3IEMzMy43MDM4NTQ1LDUuNTA4MzI0OTcgMzIuODc5MzMsNS4xNjk1ODcwNyAzMi43OTM2MTY0LDQuMjEzOTc3NTcgQzMyLjc3NTkzOCw0LjAzODY4OTQ1IDMyLjc3Mjg1NzYsMy45NjQ0MzA3OCAzMi43NzI4NTc2LDMuODY4NTEzMzQgTDMyLjc3Mjg1NzYsMS42NjYxNzg4NCBDMzIuNzcyODU3NiwxLjU3MDI2MTQgMzIuNzg4NzI4LDEuNDA5MDk4NTcgMzIuNzk5MTA3NCwxLjMxOTUwMzg3IEMzMi45MTQxNTExLDAuMzM5NzQ2ODU1IDMzLjcwMzg1NDUsMC4wMjU5NjM2MjgyIDM0LjYyOTc2MjEsMC4wMjU5NjM2MjgyIFogTTEyLjE0NDc0NDcsMC4xNjY4ODAyMTEgTDEyLjgwMjI2MTYsNC4yNjE1OTk5OCBMMTMuNDYwMTgwNCwwLjE2Njg4MDIxMSBMMTUuNTg1Mjc0NiwwLjE2Njg4MDIxMSBMMTUuNzAxOTI1NSw1LjQwNTIxMDM2IEwxNC4zOTUyNjIsNS40MDUyMTAzNiBMMTQuMzU5ODM4MiwwLjU1NTkzMTA1NCBMMTMuNDYyMjU2Miw1LjQwNTIxMDM2IEwxMi4xMzk4NTYzLDUuNDA1MjEwMzYgTDExLjI0MzA3NzksMC41NTU5MzEwNTQgTDExLjIwNzc4OCw1LjQwNTIxMDM2IEw5LjkwNDQwNTgsNS40MDUyMTAzNiBMMTAuMDE3MjM5NywwLjE2Njg4MDIxMSBMMTIuMTQ0NzQ0NywwLjE2Njg4MDIxMSBaIE03LjkwMTI1MjUsMC4xNjY4ODAyMTEgTDguODYzMjUzNTgsNS40MDUyMTAzNiBMNy40NjQzMTQxLDUuNDA1MjEwMzYgTDYuNzUzODI4ODMsMC41NTU5MzEwNTQgTDYuMDI1ODY2MDIsNS40MDUyMTAzNiBMNC42MTcxNDk4Myw1LjQwNTIxMDM2IEw1LjU4MzE2ODczLDAuMTY2ODgwMjExIEw3LjkwMTI1MjUsMC4xNjY4ODAyMTEgWiBNMjguMzA0ODM2LDUuMzUwNTkyNTcgTDI3LjAyNDYyMzMsNS4zNTA1OTI1NyBMMjcuMDI0NjIzMywwLjE2Njg4MDIxMSBMMjguOTU5ODc1MywwLjE2Njg4MDIxMSBMMzAuMTg3OTkwMyw0LjM4NDM1NTQ3IEwzMC4xMTY4NzQ4LDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDUuMzUwNTkyNTcgTDI5LjU0OTQyNDEsNS4zNTA1OTI1NyBMMjguMjMsMC45OTkgTDI4LjMwNDgzNiw1LjM1MDU5MjU3IFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTUiPjxwYXRoIGQ9Ik0yNC4zMzUyOTY2LDIuNDcwMDY0MzIgQzI1LjMwOTY1MDIsMi40NzAwNjQzMiAyNi4xMjEzMjMsMi42NTY2MDU2NCAyNi43NjkyNzY4LDMuMDI4OTczNTYgQzI3LjQxNzIzMDUsMy40MDE2OTg4MyAyNy45Mjk1MDE3LDMuOTA4NDMzNjcgMjguMzA2MDkwMiw0LjU1MDI1MDE1IEwyNi4zOTU0NTczLDUuNDgyNTk5MzcgQzI2LjE5NjA4NjksNS4xNDYzMjQ3IDI1LjkxOTE4MzYsNC44ODAwOTIzNiAyNS41NjQ3NDczLDQuNjg0NjE3MDggQzI1LjIxMDMxMTEsNC40ODkxNDE3OSAyNC44MDA0OTQyLDQuMzkxMjI1NDcgMjQuMzM1Mjk2Niw0LjM5MTIyNTQ3IEMyMy44MDM2NDIyLDQuMzkxMjI1NDcgMjMuNDEwNDM5NSw0LjQ5NDg1OTUzIDIzLjE1NTY4ODUsNC43MDIxMjc2NiBDMjIuOTAwNTkxMyw0LjkwOTM5NTc5IDIyLjc3MzU2MTksNS4xNDk1NDA5MyAyMi43NzM1NjE5LDUuNDIyMjA1NzMgQzIyLjc3MzU2MTksNS43Mzg0NjgzMSAyMi45NjE4NTYyLDUuOTY1MDMzODEgMjMuMzM4NDQ0Nyw2LjEwMDgzMDE3IEMyMy43MTQ2ODcsNi4yMzczNDEyNSAyNC4yNjg4Mzk4LDYuMzgyMDcxNTggMjQuOTk5ODY0Niw2LjUzNDY2MzgxIEMyNS4zOTg2MDU0LDYuNjExMTM4NiAyNS43OTk3NjksNi43MTE1NTY0NCAyNi4yMDQzOTQsNi44MzczNDY3NSBDMjYuNjA4NjcyOSw2Ljk2Mjc3OTcgMjYuOTc2OTU0Myw3LjEzMTgxMDQzIDI3LjMwOTIzODMsNy4zNDQ3OTYzMSBDMjcuNjQxNTIyMiw3LjU1NzQyNDgyIDI3LjkxMDExODUsNy44Mjk3MzIyNiAyOC4xMTUwMjY5LDguMTYyNzkwNyBDMjguMzE5OTM1NCw4LjQ5NTQ5MTc4IDI4LjQyMjM4OTYsOC45MTI1Mjk1NSAyOC40MjIzODk2LDkuNDE0MjYxMzcgQzI4LjQyMjM4OTYsOS43NTIzMjI4MyAyOC4zNDQ4NTY3LDEwLjEwNDMyMTMgMjguMTg5NzkwOCwxMC40Njk1NDIgQzI4LjAzNDM3ODgsMTAuODM0NzYyOCAyNy43OTM4MTkxLDExLjE3MzE4MTYgMjcuNDY3MDczMSwxMS40ODM3MjY0IEMyNy4xNDAzMjcyLDExLjc5NDk4NiAyNi43Mjc3NDEzLDEyLjA0ODM1MzQgMjYuMjI5MzE1MywxMi4yNDQ1NDM0IEMyNS43MzA4ODkzLDEyLjQ0MTA5MDggMjUuMTMyNzc4MiwxMi41MzkwMDcxIDI0LjQzNDk4MTgsMTIuNTM5MDA3MSBDMjMuMzYwNTk2OSwxMi41MzkwMDcxIDIyLjQ2ODk2ODIsMTIuMzMyODExIDIxLjc2MDA5NTcsMTEuOTIwMDYxNiBDMjEuMDUxMjIzMywxMS41MDY5NTQ4IDIwLjUwMjk1NDcsMTAuOTEwNTIyOCAyMC4xMTUyOSwxMC4xMzAwNTExIEwyMi4xOTIwNjQ5LDkuMTY1MTgyMjUgQzIyLjQyNDY2MzcsOS42MDQ3MzM2MyAyMi43NDAzMzM1LDkuOTQyNDM3NzQgMjMuMTM5MDc0MywxMC4xNzg2NTE5IEMyMy41Mzc4MTUxLDEwLjQxNDUwODggMjQuMDAzMDEyNiwxMC41MzIwNzk4IDI0LjUzNDY2NywxMC41MzIwNzk4IEMyNS4wODg0NzM2LDEwLjUzMjA3OTggMjUuNTAzODI4NiwxMC40MTc3MjUgMjUuNzgwNzMxOSwxMC4xODkwMTUzIEMyNi4wNTcyODkxLDkuOTU5OTQ4MzIgMjYuMTk2MDg2OSw5LjY4NzY0MDg4IDI2LjE5NjA4NjksOS4zNzEwMjA5NSBDMjYuMTk2MDg2OSw5LjE5NjYyOTgzIDI2LjEzMjM5OTIsOS4wNTUxMTU3MyAyNi4wMDUwMjM2LDguOTQ1NzYzOTIgQzI1Ljg3NzY0ODEsOC44MzcxMjY4MyAyNS43MTE1MDYxLDguNzQxMzU0NjYgMjUuNTA2NTk3Niw4LjY1OTg3Njg1IEMyNS4zMDEzNDMxLDguNTc4MDQxNjcgMjUuMDYwNzgzMyw4LjUxMDE0MzQ5IDI0Ljc4Mzg4LDguNDU1MTEwMjMgQzI0LjUwNjk3NjcsOC40MDA3OTE2OSAyNC4yMTg5OTcyLDguMzQwNzU1NCAyMy45MTk5NDE2LDguMjc1MzU4NzMgQzIzLjQ5ODcwMjUsOC4xODgxNjMxOCAyMy4wODY0NjI2LDguMDg0ODg2NDcgMjIuNjgyMTgzOCw3Ljk2NDgxMzkgQzIyLjI3NzkwNSw3Ljg0NTA5ODY5IDIxLjkxNTE2MTYsNy42Nzg1Njk0NyAyMS41OTM5NTM4LDcuNDY2Mjk4MzEgQzIxLjI3Mjc0NTksNy4yNTMzMTI0NCAyMS4wMTI0NTY4LDYuOTgwNjQ3NjQgMjAuODEzMDg2NCw2LjY0ODMwMzkyIEMyMC42MTM3MTYsNi4zMTU5NjAyIDIwLjUxNDAzMDgsNS44OTg5MjI0MyAyMC41MTQwMzA4LDUuMzk3NTQ3OTcgQzIwLjUxNDAzMDgsNS4wMTU1MzEzNyAyMC42MDU0MDg5LDQuNjQ3ODA5MTIgMjAuNzg4MTY1MSw0LjI5MzY2NjUgQzIwLjk3MDkyMTMsMy45MzkxNjY1MyAyMS4yMjg0NDE0LDMuNjI2MTIwMTggMjEuNTYwNzI1NCwzLjM1MzA5ODAzIEMyMS44OTMwMDkzLDMuMDgwNzkwNTkgMjIuMjk0MTczLDIuODY1NjYwNTYgMjIuNzY1MjU0OCwyLjcwNzM1MDYgQzIzLjIzNTk5MDQsMi41NDkwNDA2MyAyMy43NTkzMzc3LDIuNDcwMDY0MzIgMjQuMzM1Mjk2NiwyLjQ3MDA2NDMyIFogTTMzLjEwNzM1MTUsMi40NzAwNjQzMiBDMzQuMDgxNzA1LDIuNDcwMDY0MzIgMzQuODkzMzc3OSwyLjY1NjYwNTY0IDM1LjU0MTMzMTYsMy4wMjg5NzM1NiBDMzYuMTg5Mjg1NCwzLjQwMTY5ODgzIDM2LjcwMTU1NjUsMy45MDg0MzM2NyAzNy4wNzgxNDUxLDQuNTUwMjUwMTUgTDM1LjE2NzUxMjIsNS40ODI1OTkzNyBDMzQuOTY4MTQxOCw1LjE0NjMyNDcgMzQuNjkxMjM4NCw0Ljg4MDA5MjM2IDM0LjMzNjgwMjIsNC42ODQ2MTcwOCBDMzMuOTgyMzY1OSw0LjQ4OTE0MTc5IDMzLjU3MjU0OSw0LjM5MTIyNTQ3IDMzLjEwNzM1MTUsNC4zOTEyMjU0NyBDMzIuNTc1Njk3MSw0LjM5MTIyNTQ3IDMyLjE4MjQ5NDQsNC40OTQ4NTk1MyAzMS45Mjc3NDMzLDQuNzAyMTI3NjYgQzMxLjY3MjY0NjEsNC45MDkzOTU3OSAzMS41NDU2MTY3LDUuMTQ5NTQwOTMgMzEuNTQ1NjE2Nyw1LjQyMjIwNTczIEMzMS41NDU2MTY3LDUuNzM4NDY4MzEgMzEuNzMzOTExLDUuOTY1MDMzODEgMzIuMTEwNDk5NSw2LjEwMDgzMDE3IEMzMi40ODY3NDE5LDYuMjM3MzQxMjUgMzMuMDQwODk0Nyw2LjM4MjA3MTU4IDMzLjc3MTkxOTQsNi41MzQ2NjM4MSBDMzQuMTcwNjYwMiw2LjYxMTEzODYgMzQuNTcxODIzOSw2LjcxMTU1NjQ0IDM0Ljk3NjQ0ODksNi44MzczNDY3NSBDMzUuMzgwNzI3Nyw2Ljk2Mjc3OTcgMzUuNzQ5MDA5MSw3LjEzMTgxMDQzIDM2LjA4MTI5MzEsNy4zNDQ3OTYzMSBDMzYuNDEzNTc3MSw3LjU1NzQyNDgyIDM2LjY4MjE3MzMsNy44Mjk3MzIyNiAzNi44ODcwODE4LDguMTYyNzkwNyBDMzcuMDkxOTkwMiw4LjQ5NTQ5MTc4IDM3LjE5NDQ0NDQsOC45MTI1Mjk1NSAzNy4xOTQ0NDQ0LDkuNDE0MjYxMzcgQzM3LjE5NDQ0NDQsOS43NTIzMjI4MyAzNy4xMTY5MTE1LDEwLjEwNDMyMTMgMzYuOTYxODQ1NywxMC40Njk1NDIgQzM2LjgwNjQzMzcsMTAuODM0NzYyOCAzNi41NjU4NzM5LDExLjE3MzE4MTYgMzYuMjM5MTI4LDExLjQ4MzcyNjQgQzM1LjkxMjM4MjEsMTEuNzk0OTg2IDM1LjQ5OTc5NjEsMTIuMDQ4MzUzNCAzNS4wMDEzNzAyLDEyLjI0NDU0MzQgQzM0LjUwMjk0NDIsMTIuNDQxMDkwOCAzMy45MDQ4MzMsMTIuNTM5MDA3MSAzMy4yMDcwMzY3LDEyLjUzOTAwNzEgQzMyLjEzMjY1MTgsMTIuNTM5MDA3MSAzMS4yNDEwMjMxLDEyLjMzMjgxMSAzMC41MzIxNTA2LDExLjkyMDA2MTYgQzI5LjgyMzI3ODEsMTEuNTA2OTU0OCAyOS4yNzUwMDk1LDEwLjkxMDUyMjggMjguODg3MzQ0OSwxMC4xMzAwNTExIEwzMC45NjQxMTk4LDkuMTY1MTgyMjUgQzMxLjE5NjcxODYsOS42MDQ3MzM2MyAzMS41MTIzODgzLDkuOTQyNDM3NzQgMzEuOTExMTI5MSwxMC4xNzg2NTE5IEMzMi4zMDk4Njk5LDEwLjQxNDUwODggMzIuNzc1MDY3NSwxMC41MzIwNzk4IDMzLjMwNjcyMTgsMTAuNTMyMDc5OCBDMzMuODYwNTI4NSwxMC41MzIwNzk4IDM0LjI3NTg4MzUsMTAuNDE3NzI1IDM0LjU1Mjc4NjgsMTAuMTg5MDE1MyBDMzQuODI5MzQ0LDkuOTU5OTQ4MzIgMzQuOTY4MTQxOCw5LjY4NzY0MDg4IDM0Ljk2ODE0MTgsOS4zNzEwMjA5NSBDMzQuOTY4MTQxOCw5LjE5NjYyOTgzIDM0LjkwNDQ1NCw5LjA1NTExNTczIDM0Ljc3NzA3ODUsOC45NDU3NjM5MiBDMzQuNjQ5NzAyOSw4LjgzNzEyNjgzIDM0LjQ4MzU2MSw4Ljc0MTM1NDY2IDM0LjI3ODY1MjUsOC42NTk4NzY4NSBDMzQuMDczMzk3OSw4LjU3ODA0MTY3IDMzLjgzMjgzODIsOC41MTAxNDM0OSAzMy41NTU5MzQ4LDguNDU1MTEwMjMgQzMzLjI3OTAzMTUsOC40MDA3OTE2OSAzMi45OTEwNTIxLDguMzQwNzU1NCAzMi42OTE5OTY1LDguMjc1MzU4NzMgQzMyLjI3MDc1NzMsOC4xODgxNjMxOCAzMS44NTg1MTc1LDguMDg0ODg2NDcgMzEuNDU0MjM4Niw3Ljk2NDgxMzkgQzMxLjA0OTk1OTgsNy44NDUwOTg2OSAzMC42ODcyMTY1LDcuNjc4NTY5NDcgMzAuMzY2MDA4Niw3LjQ2NjI5ODMxIEMzMC4wNDQ4MDA4LDcuMjUzMzEyNDQgMjkuNzg0NTExNiw2Ljk4MDY0NzY0IDI5LjU4NTE0MTIsNi42NDgzMDM5MiBDMjkuMzg1NzcwOSw2LjMxNTk2MDIgMjkuMjg2MDg1Nyw1Ljg5ODkyMjQzIDI5LjI4NjA4NTcsNS4zOTc1NDc5NyBDMjkuMjg2MDg1Nyw1LjAxNTUzMTM3IDI5LjM3NzQ2MzgsNC42NDc4MDkxMiAyOS41NjAyMTk5LDQuMjkzNjY2NSBDMjkuNzQyOTc2MSwzLjkzOTE2NjUzIDMwLjAwMDQ5NjIsMy42MjYxMjAxOCAzMC4zMzI3ODAyLDMuMzUzMDk4MDMgQzMwLjY2NTA2NDIsMy4wODA3OTA1OSAzMS4wNjYyMjc5LDIuODY1NjYwNTYgMzEuNTM3MzA5NiwyLjcwNzM1MDYgQzMyLjAwODA0NTMsMi41NDkwNDA2MyAzMi41MzEzOTI2LDIuNDcwMDY0MzIgMzMuMTA3MzUxNSwyLjQ3MDA2NDMyIFogTTEzLjc3MzIwNTcsMi40NzAwMjg1OSBDMTQuNDE1NjIxNCwyLjQ3MDAyODU5IDE1LjAwODE5NDUsMi41OTAxMDExNiAxNS41NTA5MjUsMi44Mjk1MzE1OSBDMTYuMDkzMzA5NCwzLjA2OTY3NjczIDE2LjU0MjIzODksMy4zOTY2NjAwNyAxNi44OTY2NzUxLDMuODEwODM4OTcgTDE2Ljg5NjY3NTEsMi40ODcxODE4MSBMMTkuMTM5NTkyLDIuNDg3MTgxODEgTDE5LjEzOTU5MiwxMi41MjE4MTgxIEwxNi44OTY2NzUxLDEyLjUyMTgxODEgTDE2Ljg5NjY3NTEsMTEuMDk1OTU2MyBDMTYuNTQyMjM4OSwxMS41NDQwODQzIDE2LjA4ODExNzQsMTEuODk2Nzk3NSAxNS41MzQzMTA4LDEyLjE1MzczODUgQzE0Ljk4MDUwNDIsMTIuNDEwNjc5NSAxNC4zODIzOTMsMTIuNTM4OTcxNCAxMy43Mzk5NzczLDEyLjUzODk3MTQgQzEzLjE1Mjk0MjMsMTIuNTM4OTcxNCAxMi41NzQyMTQzLDEyLjQyNjQwMzMgMTIuMDAzNzkzNSwxMi4yMDA1NTI1IEMxMS40MzMzNzI2LDExLjk3NDM0NDQgMTAuOTIxMTAxNSwxMS42NDYyODkgMTAuNDY2OTgwMSwxMS4yMTYzODYzIEMxMC4wMTI4NTg2LDEwLjc4NjQ4MzYgOS42NDcwMDAxMSwxMC4yNjAwOTQgOS4zNzA0NDI5Miw5LjYzNzU3NDkxIEM5LjA5MzUzOTYsOS4wMTQ2OTg0NCA4Ljk1NTA4Nzk0LDguMzA2NDEzMjIgOC45NTUwODc5NCw3LjUxMzA3NjU4IEM4Ljk1NTA4Nzk0LDYuNzA4MzA0NDcgOS4wOTA0MjQ0NCw1Ljk5NTAxNjIyIDkuMzYyMTM1ODIsNS4zNzE3ODI0IEM5LjYzMzUwMTA3LDQuNzQ4OTA1OTMgOS45OTM0NzUzOSw0LjIyMjg3MzcxIDEwLjQ0MjA1ODgsMy43OTI5NzEwMyBDMTAuODkwNjQyMSwzLjM2MjcxMDk4IDExLjQwNTY4MjMsMy4wMzUwMTI5MiAxMS45ODcxNzkzLDIuODA5NTE5NDkgQzEyLjU2ODY3NjMsMi41ODMzMTEzNCAxMy4xNjQwMTg0LDIuNDcwMDI4NTkgMTMuNzczMjA1NywyLjQ3MDAyODU5IFogTTQuMTUzNTQ5NzgsMCBDNC43ODQ4ODkzNSwwIDUuMzY2Mzg2MzIsMC4xMTcyMTM3MDEgNS44OTgwNDA2OSwwLjM1MTk5ODQ2MSBDNi40Mjk2OTUwNiwwLjU4NjQyNTg2MiA2Ljg4OTAwODQ0LDAuOTAzNDAzMTU2IDcuMjc3MDE5MjIsMS4zMDQwMDI0MiBDNy42NjQ2ODM4NiwxLjcwNDI0NDMyIDcuOTY4OTMxMzgsMi4xNzU5NTggOC4xOTA4MDAxNywyLjcxOTE0MzQ0IEM4LjQxMjMyMjgyLDMuMjYxOTcxNTIgOC41MjMwODQxNSwzLjg0MjMyMjI4IDguNTIzMDg0MTUsNC40NjAxOTU3MiBDOC41MjMwODQxNSw1LjA3NzM1NDQ0IDguNDEyMzIyODIsNS42NjA1NjQwOCA4LjE5MDgwMDE3LDYuMjA5NDY3MjYgQzcuOTY4OTMxMzgsNi43NTgzNzA0NCA3LjY2NDY4Mzg2LDcuMjMyOTQyOTkgNy4yNzcwMTkyMiw3LjYzMzU0MjI1IEM2Ljg4OTAwODQ0LDguMDMzNzg0MTYgNi40MjY5MjYwMyw4LjM1MTExODgxIDUuODg5NzMzNTksOC41ODUxODg4NSBDNS4zNTIxOTUwMiw4LjgxOTYxNjI1IDQuNzY4Mjc1MTUsOC45MzY4Mjk5NSA0LjEzNjkzNTU4LDguOTM2ODI5OTUgTDIuMjU5NTMxMDgsOC45MzY4Mjk5NSBMMi4yNTk1MzEwOCwxMi41MjE4NTM5IEwwLDEyLjUyMTg1MzkgTDAsMCBMNC4xNTM1NDk3OCwwIFogTTE0LjEwNTQ4OTcsNC41ODAyMzI1NiBDMTMuNjk1NjcyOCw0LjU4MDIzMjU2IDEzLjMxMDc3NzEsNC42NTU2MzUyNyAxMi45NTA4MDI4LDQuODA1NzI1OTkgQzEyLjU5MDgyODUsNC45NTU4MTY3IDEyLjI3NzkyNzgsNS4xNjAyMjU5NiAxMi4wMTIxMDA2LDUuNDE3NTI0MzMgQzExLjc0NjI3MzQsNS42NzU1Mzc0MSAxMS41Mzg1OTU5LDUuOTgxNzkzOTQgMTEuMzg5MDY4MSw2LjMzNjI5MzkxIEMxMS4yMzk1NDAzLDYuNjkwNzkzODkgMTEuMTY0Nzc2NCw3LjA3MTczODQxIDExLjE2NDc3NjQsNy40ODAxOTk1NyBDMTEuMTY0Nzc2NCw3Ljg4ODMwMzM3IDExLjIzOTU0MDMsOC4yNzI0NjQxMyAxMS4zODkwNjgxLDguNjMxOTY3MTIgQzExLjUzODU5NTksOC45OTE0NzAxMiAxMS43NDYyNzM0LDkuMzAyNzI5NjcgMTIuMDEyMTAwNiw5LjU2NjQ2MDUgQzEyLjI3NzkyNzgsOS44Mjk0NzY2MSAxMi41OTA4Mjg1LDEwLjAzNjAzIDEyLjk1MDgwMjgsMTAuMTg2ODM1NCBDMTMuMzEwNzc3MSwxMC4zMzY5MjYyIDEzLjY5NTY3MjgsMTAuNDExOTcxNSAxNC4xMDU0ODk3LDEwLjQxMTk3MTUgQzE0LjUyNjM4MjcsMTAuNDExOTcxNSAxNC45MTM3MDEyLDEwLjMzNjkyNjIgMTUuMjY4NDgzNiwxMC4xODY4MzU0IEMxNS42MjI5MTk5LDEwLjAzNjAzIDE1LjkyNzE2NzQsOS44MjY2MTc3NCAxNi4xODIyNjQ2LDkuNTU3ODgzODkgQzE2LjQzNzAxNTYsOS4yODk1MDczOSAxNi42MzkxNTUsOC45Nzc4OTA0OCAxNi43ODg2ODI4LDguNjIzNzQ3ODcgQzE2LjkzODIxMDYsOC4yNjk2MDUyNiAxNy4wMTI5NzQ1LDcuODg4MzAzMzcgMTcuMDEyOTc0NSw3LjQ4MDE5OTU3IEMxNy4wMTI5NzQ1LDcuMDgyODE2NTQgMTYuOTM4MjEwNiw2LjcwNjg3NTAzIDE2Ljc4ODY4MjgsNi4zNTIzNzUwNiBDMTYuNjM5MTU1LDUuOTk3ODc1MDkgMTYuNDM3MDE1Niw1LjY4OTExNzA1IDE2LjE4MjI2NDYsNS40MjYxMDA5NCBDMTUuOTI3MTY3NCw1LjE2MjcyNzQ3IDE1LjYyMjkxOTksNC45NTU4MTY3IDE1LjI2ODQ4MzYsNC44MDU3MjU5OSBDMTQuOTEzNzAxMiw0LjY1NTYzNTI3IDE0LjUyNjM4MjcsNC41ODAyMzI1NiAxNC4xMDU0ODk3LDQuNTgwMjMyNTYgWiBNMy45ODc0MDc3OSwyLjE2MTMwNjI4IEwyLjI1OTUzMTA4LDIuMTYxMzA2MjggTDIuMjU5NTMxMDgsNi43NzU1MjM2NyBMMy45ODc0MDc3OSw2Ljc3NTUyMzY3IEM0LjMxOTY5MTc3LDYuNzc1NTIzNjcgNC42MjQyODU0Miw2LjcxNTQ4NzM4IDQuOTAxMTg4NzQsNi41OTU0MTQ4MSBDNS4xNzc3NDU5Myw2LjQ3NTM0MjI0IDUuNDE2MjI4OTIsNi4zMDk1Mjc3NCA1LjYxNTU5OTMsNi4wOTgzMjg2NiBDNS44MTQ5Njk2OSw1Ljg4Njc3MjIyIDUuOTcwMDM1NTUsNS42NDA1NTE5OCA2LjA4MDc5Njg4LDUuMzYwMzgyNjUgQzYuMTkxMjEyMDgsNS4wODA1NzA2NyA2LjI0NjkzODg3LDQuNzgwMDMxODkgNi4yNDY5Mzg4Nyw0LjQ2MDE5NTcyIEM2LjI0NjkzODg3LDQuMTM5NjQ0ODQgNi4xOTEyMTIwOCwzLjgzOTQ2MzQxIDYuMDgwNzk2ODgsMy41NTkyOTQwOCBDNS45NzAwMzU1NSwzLjI3OTEyNDc1IDUuODE0OTY5NjksMy4wMzYxMjA3MyA1LjYxNTU5OTMsMi44MzAyODIwNCBDNS40MTYyMjg5MiwyLjYyNDQ0MzM0IDUuMTc3NzQ1OTMsMi40NjE0ODc3MSA0LjkwMTE4ODc0LDIuMzQxNDE1MTQgQzQuNjI0Mjg1NDIsMi4yMjEzNDI1NyA0LjMxOTY5MTc3LDIuMTYxMzA2MjggMy45ODc0MDc3OSwyLjE2MTMwNjI4IFoiPjwvcGF0aD48L2NsaXBQYXRoPjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjMzLjAgLTE2MC4wKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2kwKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjMzLjAgMTYwLjApIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMC4wMDAxODA1NTU1NTU1NTIwNzU0OCAwLjApIj48ZyBjbGlwLXBhdGg9InVybCgjaTEpIj48cG9seWdvbiBwb2ludHM9IjAsMCA1MiwwIDUyLDUyIDAsNTIgMCwwIiBzdHJva2U9Im5vbmUiIGZpbGw9InVybCgjaTIpIj48L3BvbHlnb24+PC9nPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3LjU4MzMzMzMzMzMzMzMzMiAxNC44MDU1NTU1NTU1NTU1NSkiPjxnIGNsaXAtcGF0aD0idXJsKCNpMykiPjxnIGNsaXAtcGF0aD0idXJsKCNpNCkiPjxwb2x5Z29uIHBvaW50cz0iMCwwIDM2LjQ3OTE2NjcsMCAzNi40NzkxNjY3LDUuNjEyMTc5NDkgMCw1LjYxMjE3OTQ5IDAsMCIgc3Ryb2tlPSJub25lIiBmaWxsPSIjRkZGRkZGIj48L3BvbHlnb24+PC9nPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNy41ODMzMzMzMzMzMzMzNzEgMjQuNDMyMDgwMjU1NDc3MzMpIj48ZyBjbGlwLXBhdGg9InVybCgjaTUpIj48cG9seWdvbiBwb2ludHM9IjAsMCAzNy4xOTQ0NDQ0LDAgMzcuMTk0NDQ0NCwxMi41MzkwMDcxIDAsMTIuNTM5MDA3MSAwLDAiIHN0cm9rZT0ibm9uZSIgZmlsbD0iI0ZGRkZGRiI+PC9wb2x5Z29uPjwvZz48L2c+PC9nPjwvZz48L2c+PC9zdmc+", + "icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgd2lkdGg9IjUycHgiIGhlaWdodD0iNTJweCIgdmlld0JveD0iMCAwIDUyLjAgNTIuMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PGNsaXBQYXRoIGlkPSJpMCI+PHBhdGggZD0iTTM2MCwwIEwzNjAsODAwIEwwLDgwMCBMMCwwIEwzNjAsMCBaIj48L3BhdGg+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkxIj48cGF0aCBkPSJNMjYsMCBDMzMuOTkxMDI3OCwwIDQxLjEzOTU4MzMsMC45NzUgNDUuOTA4Nzc3OCw1Ljc3Nzc3Nzc4IEM0OS43MTAxOTQ0LDkuNjA1OTE2NjcgNTIsMTUuODY1MDU1NiA1MiwyNiBDNTIsMzYuMTM0OTQ0NCA0OS43MDk4MzMzLDQyLjM5NDQ0NDQgNDUuOTA4MDU1Niw0Ni4yMjI1ODMzIEM0MS4xMzg4NjExLDUxLjAyNDYzODkgMzMuOTkwMzA1Niw1MiAyNiw1MiBDMTguMDA4OTcyMiw1MiAxMC44NjA3Nzc4LDUxLjAyNDYzODkgNi4wOTE1ODMzMyw0Ni4yMjI1ODMzIEMyLjI5MDE2NjY3LDQyLjM5NDQ0NDQgMCwzNi4xMzQ5NDQ0IDAsMjYgQzAsMTUuODY1MDU1NiAyLjI4OTgwNTU2LDkuNjA1NTU1NTYgNi4wOTA4NjExMSw1Ljc3Nzc3Nzc4IEMxMC44NjAwNTU2LDAuOTc1IDE4LjAwODYxMTEsMCAyNiwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxsaW5lYXJHcmFkaWVudCBpZD0iaTIiIHgxPSIyNnB4IiB5MT0iNTJweCIgeDI9IjI2cHgiIHkyPSIwLjE5NTkyMTE0OHB4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzI5MjlCMiIgb2Zmc2V0PSIwJSI+PC9zdG9wPjxzdG9wIHN0b3AtY29sb3I9IiMxQTQwQ0MiIG9mZnNldD0iMTAwJSI+PC9zdG9wPjwvbGluZWFyR3JhZGllbnQ+PGNsaXBQYXRoIGlkPSJpMyI+PHBhdGggZD0iTTM3LjE5NDQ0NDQsMCBMMzcuMTk0NDQ0NCw1LjcyMjIyMjIyIEwwLDUuNzIyMjIyMjIgTDAsMCBMMzcuMTk0NDQ0NCwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTQiPjxwYXRoIGQ9Ik0xLjg4MzQyMjEzLDAgQzIuNjIwNzU5ODcsMCAzLjY0MzU2NDAyLDAuMTgxNjEwODcxIDMuNjQzNTY0MDIsMS4zMjExMTgxOSBMMy42NDM1NjQwMiwxLjY4OTExNTYyIEwyLjM0Mjc5MzM5LDEuNjg5MTE1NjIgTDIuMzQyNzkzMzksMS4zNjQ5MDY1OSBDMi4zNDI3OTMzOSwxLjA3OTU3NTczIDIuMTYzMTk2NjQsMC44ODkxNTMzNzEgMS44NTgxNzY4LDAuODg5MTUzMzcxIEMxLjUyOTMxNzg4LDAuODg5MTUzMzcxIDEuNDE2NDgzOTgsMS4wNzUyNzA4OCAxLjM4MDg1OTI3LDEuMjQyMTUxMDkgQzEuMzY2Nzk2ODgsMS4zMDAyNjY1NyAxLjM2MDkwNDA4LDEuNDExNzIxODQgMS4zODYxNDk0MSwxLjUxNzI1NzkzIEMxLjUzNDYwODAyLDIuMTMwNDk3MyAzLjUxMjQ0OTAyLDIuNDU2MTE4ODcgMy43MzI1NTg4MywzLjU1NTUzNzI3IEMzLjc1MzcxOTM3LDMuNjY3MDU5OCAzLjgwMDc5NDg4LDMuOTYxMTM0ODggMy43MzgwNDk4Niw0LjQxMzAwOTYzIEMzLjYxMTgyMzIxLDUuMjg5NjUyMDMgMi44MzgwNTcyLDUuNjEyMTc5NDkgMS44OTU4MTA0Myw1LjYxMjE3OTQ5IEMwLjkxNTcyOTEzNiw1LjYxMjE3OTQ5IDAsNS4yNTk5ODg5MiAwLDQuMDg4ODAwNiBMMCwzLjY4ODI0NzczIEwxLjM5OTQwODIzLDMuNjg4MjQ3NzMgTDEuNDAxMjE2MjUsNC4xOTIzMTg3OSBDMS40MDEyMTYyNSw0LjQ3ODY1ODYgMS41OTYwODA3Myw0LjY2OTI4Mjc1IDEuOTIxODU5MzIsNC42NjkyODI3NSBDMi4yNzA3NDA0LDQuNjY5MjgyNzUgMi4zODgzOTU2OSw0LjQ5MTUwNTg5IDIuNDMxMTg1NTIsNC4zMTU0MTA2MSBDMi40NTYyMjk5Niw0LjIxNjM5OTA1IDIuNDcxNDk3NjksNC4wNTQ2MzA4NSAyLjQyMTAwNzAzLDMuOTI4NjQ2NzEgQzIuMTUxMzQ0MDYsMy4yNTA5NjkxMSAwLjI5NzkyMTY3NywyLjk0MTI4ODk1IDAuMDcyMTE5OTQ3MywxLjg3MDMyMjkyIEMwLjAxNzM0MzYwODUsMS42MDU2NDE4OSAwLjAyMzgzOTA5MTIsMS4zOTkwNzYzNCAwLjA2MTc0MDU2NzcsMS4xNjQyNjAyMSBDMC4yMDAyMjE1ODEsMC4zMDg4NzMwMDcgMC45NTcxMTI3MjcsMCAxLjg4MzQyMjEzLDAgWiBNMTguODQxMTE4NiwwLjAyOTY2MzEwODkgQzE5LjU3MDQ4NzcsMC4wMjk2NjMxMDg5IDIwLjU3Nzg5MDEsMC4yMDY5NjkxMjkgMjAuNTc3ODkwMSwxLjMzNTY0NzA2IEwyMC41Nzc4OTAxLDEuNzAwNzUyMTcgTDE5LjI5MjE4NjQsMS43MDA3NTIxNyBMMTkuMjkyMTg2NCwxLjM4MDQ0NDQxIEMxOS4yOTIxODY0LDEuMDk3NDAwNSAxOS4xMTU2MDMsMC45MDg3OTQyNSAxOC44MTQ0MDAxLDAuOTA4Nzk0MjUgQzE4LjQ5MTIzMzEsMC45MDg3OTQyNSAxOC4zNzkwNjg4LDEuMDkxNjE1ODYgMTguMzQxNzcsMS4yNTkzNzA0OSBDMTguMzI5NzgzNSwxLjMxNjgxMzM0IDE4LjMyMzA4NzEsMS40MjY2NTQyOCAxOC4zNDYyNTY2LDEuNTMwMTcyNDggQzE4LjQ5NDMxMzQsMi4xMzY0MTY0NyAyMC40NTAzOTEyLDIuNDYzMTE0MjUgMjAuNjY2MDE0NCwzLjU0OTIxNDUyIEMyMC42ODk2NTI2LDMuNjU5MDU1NDcgMjAuNzM0MDQ5NiwzLjk1MDcwOTA3IDIwLjY3NTE4ODUsNC4zOTg0MTM1IEMyMC41NTE3NzQzLDUuMjY1MTAwOTMgMTkuNzgyNDk0OSw1LjU4NDgwMzMzIDE4Ljg1MTA5NjIsNS41ODQ4MDMzMyBDMTcuODc4NTgxOCw1LjU4NDgwMzMzIDE2Ljk3NTY0MjgsNS4yMzU0Mzc4MyAxNi45NzU2NDI4LDQuMDc3NTAwMzcgTDE2Ljk3NTY0MjgsMy42Nzc4ODkxOSBMMTguMzU5NTE1NCwzLjY3Nzg4OTE5IEwxOC4zNTk5MTcyLDQuMTgwNjE0OTggQzE4LjM1OTkxNzIsNC40NjMwNTM1MiAxOC41NTUxODM0LDQuNjUwNDQ5MDMgMTguODc5NzU2Nyw0LjY1MDQ0OTAzIEMxOS4yMjU3NTgzLDQuNjUwNDQ5MDMgMTkuMzQyNDc2MSw0LjQ3NTE2MDkxIDE5LjM4MjM4NjUsNC4zMDA4ODE3NCBDMTkuNDA2MzU5NSw0LjIwNTU2OTY2IDE5LjQxOTgxOTIsNC4wNDM4MDE0NiAxOS4zNzI4MTA3LDMuOTE2OTQyOSBDMTkuMTA2NDI4OSwzLjI0NzI2OTYzIDE3LjI3MTE1MzcsMi45NDAwNzgyMSAxNy4wNDc3NjI3LDEuODgxMTUyMyBDMTYuOTkwMTA2OSwxLjYxOTM2MzYgMTYuOTk5MDgwMSwxLjQxNDIxMDU4IDE3LjAzNTMwNzQsMS4xODMwOTM5MyBDMTcuMTcyMzE1MywwLjMzMzgyNzY4NiAxNy45MjI1MSwwLjAyOTY2MzEwODkgMTguODQxMTE4NiwwLjAyOTY2MzEwODkgWiBNMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBMMjMuMjM2MjI5MSw0LjExMTM3MzY2IEMyMy4yMzcyMDYzLDQuMTU3OTg0NjIgMjMuMjQwODgxOCw0LjIwNDA2NzQ1IDIzLjI0OTY3NjQsNC4yNDExNTE5NCBDMjMuMjc1MTIyNiw0LjM3MTIzOTEzIDIzLjM4Nzc1NTYsNC42MjE1OTMwOCAyMy43NDY5NDkxLDQuNjIxNTkzMDggQzI0LjExMDgzMDEsNC42MjE1OTMwOCAyNC4yMjA1ODM2LDQuMzcxMjM5MTMgMjQuMjQ4MTA1Nyw0LjI0MTE1MTk0IEMyNC4yNTk2OTA1LDQuMTg1NTI1MiAyNC4yNjE3NjYzLDQuMTA5NjUyMjIgMjQuMjU5NjkwNSw0LjA0MjExOTg4IEwyNC4yNTk2OTA1LDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDMuOTIxODUzMTIgQzI1LjU4NDMwMDIsNC4wMTg2NDQ5OSAyNS41NzQ3MjQ0LDQuMjE2Mzk5MDUgMjUuNTY3NDI1Myw0LjI2ODE5MTc4IEMyNS40NzQ3NDc1LDUuMjQ2NjcwNzkgMjQuNzA3NDc3LDUuNTY0MzU1MjkgMjMuNzQ2OTQ5MSw1LjU2NDM1NTI5IEMyMi43ODgyOTYyLDUuNTY0MzU1MjkgMjIuMDIwNTU3LDUuMjQ2NjcwNzkgMjEuOTI5NTUzMiw0LjI2ODE5MTc4IEMyMS45MjM4NjEzLDQuMjE2Mzk5MDUgMjEuOTE2Mjk0NCw0LjAxODY0NDk5IDIxLjkxNzk2ODUsMy45MjE4NTMxMiBMMjEuOTE3OTY4NSwwLjE2Njg4MDIxMSBMMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBaIE0zNC42Mjk3NjIxLDAuMDI1OTYzNjI4MiBDMzUuNTUzOTk1NiwwLjAyNTk2MzYyODIgMzYuMzYwMDM4MiwwLjMzNjg1NDUzNCAzNi40NTg3NDI3LDEuMzE5NTAzODcgQzM2LjQ2NTU3MywxLjM5MTE5NjkyIDM2LjQ2ODM4MTQsMS40NjUxMTM3OCAzNi40NjkzNjA3LDEuNTI2MTgwMjIgTDM2LjQ2OTAwNCwxLjY1NTc1NDAyIEwzNi40Njg3MjAzLDEuNjY2MTc4ODQgTDM2LjQ2ODcyMDMsMS44MzgwMzY1NCBMMzUuMTU1MzYwNSwxLjgzODAzNjU0IEwzNS4xNTUxNSwxLjUzMzU1NTU2IEMzNS4xNTQ0NDcxLDEuNDk4NzMzNjIgMzUuMTUxNDksMS40MDU2NDEyMyAzNS4xMzk0OTAxLDEuMzQ1MjY1NzEgQzM1LjExNjY1NTUsMS4yMzEzMjE3IDM1LjAxNzA4MDQsMC45NjYwMzUzMDYgMzQuNjE4MTc3NCwwLjk2NjAzNTMwNiBDMzQuMjM4MTU4MiwwLjk2NjAzNTMwNiAzNC4xMjU1OTIxLDEuMjE3NTk5OTkgMzQuMDk5Njc3MiwxLjM0NTI2NTcxIEMzNC4wODE3OTc4LDEuNDE1NDIxMzIgMzQuMDc2MTA1OSwxLjUwODExMDEyIDM0LjA3NjEwNTksMS41OTI3OTQ2IEwzNC4wNzYxMDU5LDMuOTg2ODk2NzIgQzM0LjA3NjEwNTksNC4wNTQ2MzA4NSAzNC4wODAxOTA3LDQuMTI3NjExNTEgMzQuMDg5NzY2NSw0LjE4NjEzMDU3IEMzNC4xMTI1MzQyLDQuMzI3NTE4IDM0LjI0Mzg1MDEsNC41NjgyNTMzIDM0LjYyMDk4OTksNC41NjgyNTMzIEMzNS4wMDA0MDY0LDQuNTY4MjUzMyAzNS4xMzQxMzMsNC4zMjc1MTggMzUuMTU1MzYwNSw0LjE4NjEzMDU3IEMzNS4xNjY5NDUyLDQuMTI3NjExNTEgMzUuMTcwNjI4Miw0LjA1NDYzMDg1IDM1LjE2ODk1NDEsMy45ODY4OTY3MiBMMzUuMTY4OTU0MSwzLjIyODkwNjc2IEwzNC42MzQ0NDk2LDMuMjI4OTA2NzYgTDM0LjYzNDQ0OTYsMi40NjQ5MzAzNiBMMzYuNDc5MTY2NywyLjQ2NDkzMDM2IEwzNi40NzkxNjY3LDMuODY4NTEzMzQgQzM2LjQ3NzA5MDgsMy45NjQ0MzA3OCAzNi40NzU0ODM3LDQuMDM4Njg5NDUgMzYuNDYwMDE1LDQuMjEzOTc3NTcgQzM2LjM3MjgyODIsNS4xNjk1ODcwNyAzNS41NTM5OTU2LDUuNTA4MzI0OTcgMzQuNjI2MDc5MSw1LjUwODMyNDk3IEMzMy43MDM4NTQ1LDUuNTA4MzI0OTcgMzIuODc5MzMsNS4xNjk1ODcwNyAzMi43OTM2MTY0LDQuMjEzOTc3NTcgQzMyLjc3NTkzOCw0LjAzODY4OTQ1IDMyLjc3Mjg1NzYsMy45NjQ0MzA3OCAzMi43NzI4NTc2LDMuODY4NTEzMzQgTDMyLjc3Mjg1NzYsMS42NjYxNzg4NCBDMzIuNzcyODU3NiwxLjU3MDI2MTQgMzIuNzg4NzI4LDEuNDA5MDk4NTcgMzIuNzk5MTA3NCwxLjMxOTUwMzg3IEMzMi45MTQxNTExLDAuMzM5NzQ2ODU1IDMzLjcwMzg1NDUsMC4wMjU5NjM2MjgyIDM0LjYyOTc2MjEsMC4wMjU5NjM2MjgyIFogTTEyLjE0NDc0NDcsMC4xNjY4ODAyMTEgTDEyLjgwMjI2MTYsNC4yNjE1OTk5OCBMMTMuNDYwMTgwNCwwLjE2Njg4MDIxMSBMMTUuNTg1Mjc0NiwwLjE2Njg4MDIxMSBMMTUuNzAxOTI1NSw1LjQwNTIxMDM2IEwxNC4zOTUyNjIsNS40MDUyMTAzNiBMMTQuMzU5ODM4MiwwLjU1NTkzMTA1NCBMMTMuNDYyMjU2Miw1LjQwNTIxMDM2IEwxMi4xMzk4NTYzLDUuNDA1MjEwMzYgTDExLjI0MzA3NzksMC41NTU5MzEwNTQgTDExLjIwNzc4OCw1LjQwNTIxMDM2IEw5LjkwNDQwNTgsNS40MDUyMTAzNiBMMTAuMDE3MjM5NywwLjE2Njg4MDIxMSBMMTIuMTQ0NzQ0NywwLjE2Njg4MDIxMSBaIE03LjkwMTI1MjUsMC4xNjY4ODAyMTEgTDguODYzMjUzNTgsNS40MDUyMTAzNiBMNy40NjQzMTQxLDUuNDA1MjEwMzYgTDYuNzUzODI4ODMsMC41NTU5MzEwNTQgTDYuMDI1ODY2MDIsNS40MDUyMTAzNiBMNC42MTcxNDk4Myw1LjQwNTIxMDM2IEw1LjU4MzE2ODczLDAuMTY2ODgwMjExIEw3LjkwMTI1MjUsMC4xNjY4ODAyMTEgWiBNMjguMzA0ODM2LDUuMzUwNTkyNTcgTDI3LjAyNDYyMzMsNS4zNTA1OTI1NyBMMjcuMDI0NjIzMywwLjE2Njg4MDIxMSBMMjguOTU5ODc1MywwLjE2Njg4MDIxMSBMMzAuMTg3OTkwMyw0LjM4NDM1NTQ3IEwzMC4xMTY4NzQ4LDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDUuMzUwNTkyNTcgTDI5LjU0OTQyNDEsNS4zNTA1OTI1NyBMMjguMjMsMC45OTkgTDI4LjMwNDgzNiw1LjM1MDU5MjU3IFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTUiPjxwYXRoIGQ9Ik0yNC4zMzUyOTY2LDIuNDcwMDY0MzIgQzI1LjMwOTY1MDIsMi40NzAwNjQzMiAyNi4xMjEzMjMsMi42NTY2MDU2NCAyNi43NjkyNzY4LDMuMDI4OTczNTYgQzI3LjQxNzIzMDUsMy40MDE2OTg4MyAyNy45Mjk1MDE3LDMuOTA4NDMzNjcgMjguMzA2MDkwMiw0LjU1MDI1MDE1IEwyNi4zOTU0NTczLDUuNDgyNTk5MzcgQzI2LjE5NjA4NjksNS4xNDYzMjQ3IDI1LjkxOTE4MzYsNC44ODAwOTIzNiAyNS41NjQ3NDczLDQuNjg0NjE3MDggQzI1LjIxMDMxMTEsNC40ODkxNDE3OSAyNC44MDA0OTQyLDQuMzkxMjI1NDcgMjQuMzM1Mjk2Niw0LjM5MTIyNTQ3IEMyMy44MDM2NDIyLDQuMzkxMjI1NDcgMjMuNDEwNDM5NSw0LjQ5NDg1OTUzIDIzLjE1NTY4ODUsNC43MDIxMjc2NiBDMjIuOTAwNTkxMyw0LjkwOTM5NTc5IDIyLjc3MzU2MTksNS4xNDk1NDA5MyAyMi43NzM1NjE5LDUuNDIyMjA1NzMgQzIyLjc3MzU2MTksNS43Mzg0NjgzMSAyMi45NjE4NTYyLDUuOTY1MDMzODEgMjMuMzM4NDQ0Nyw2LjEwMDgzMDE3IEMyMy43MTQ2ODcsNi4yMzczNDEyNSAyNC4yNjg4Mzk4LDYuMzgyMDcxNTggMjQuOTk5ODY0Niw2LjUzNDY2MzgxIEMyNS4zOTg2MDU0LDYuNjExMTM4NiAyNS43OTk3NjksNi43MTE1NTY0NCAyNi4yMDQzOTQsNi44MzczNDY3NSBDMjYuNjA4NjcyOSw2Ljk2Mjc3OTcgMjYuOTc2OTU0Myw3LjEzMTgxMDQzIDI3LjMwOTIzODMsNy4zNDQ3OTYzMSBDMjcuNjQxNTIyMiw3LjU1NzQyNDgyIDI3LjkxMDExODUsNy44Mjk3MzIyNiAyOC4xMTUwMjY5LDguMTYyNzkwNyBDMjguMzE5OTM1NCw4LjQ5NTQ5MTc4IDI4LjQyMjM4OTYsOC45MTI1Mjk1NSAyOC40MjIzODk2LDkuNDE0MjYxMzcgQzI4LjQyMjM4OTYsOS43NTIzMjI4MyAyOC4zNDQ4NTY3LDEwLjEwNDMyMTMgMjguMTg5NzkwOCwxMC40Njk1NDIgQzI4LjAzNDM3ODgsMTAuODM0NzYyOCAyNy43OTM4MTkxLDExLjE3MzE4MTYgMjcuNDY3MDczMSwxMS40ODM3MjY0IEMyNy4xNDAzMjcyLDExLjc5NDk4NiAyNi43Mjc3NDEzLDEyLjA0ODM1MzQgMjYuMjI5MzE1MywxMi4yNDQ1NDM0IEMyNS43MzA4ODkzLDEyLjQ0MTA5MDggMjUuMTMyNzc4MiwxMi41MzkwMDcxIDI0LjQzNDk4MTgsMTIuNTM5MDA3MSBDMjMuMzYwNTk2OSwxMi41MzkwMDcxIDIyLjQ2ODk2ODIsMTIuMzMyODExIDIxLjc2MDA5NTcsMTEuOTIwMDYxNiBDMjEuMDUxMjIzMywxMS41MDY5NTQ4IDIwLjUwMjk1NDcsMTAuOTEwNTIyOCAyMC4xMTUyOSwxMC4xMzAwNTExIEwyMi4xOTIwNjQ5LDkuMTY1MTgyMjUgQzIyLjQyNDY2MzcsOS42MDQ3MzM2MyAyMi43NDAzMzM1LDkuOTQyNDM3NzQgMjMuMTM5MDc0MywxMC4xNzg2NTE5IEMyMy41Mzc4MTUxLDEwLjQxNDUwODggMjQuMDAzMDEyNiwxMC41MzIwNzk4IDI0LjUzNDY2NywxMC41MzIwNzk4IEMyNS4wODg0NzM2LDEwLjUzMjA3OTggMjUuNTAzODI4NiwxMC40MTc3MjUgMjUuNzgwNzMxOSwxMC4xODkwMTUzIEMyNi4wNTcyODkxLDkuOTU5OTQ4MzIgMjYuMTk2MDg2OSw5LjY4NzY0MDg4IDI2LjE5NjA4NjksOS4zNzEwMjA5NSBDMjYuMTk2MDg2OSw5LjE5NjYyOTgzIDI2LjEzMjM5OTIsOS4wNTUxMTU3MyAyNi4wMDUwMjM2LDguOTQ1NzYzOTIgQzI1Ljg3NzY0ODEsOC44MzcxMjY4MyAyNS43MTE1MDYxLDguNzQxMzU0NjYgMjUuNTA2NTk3Niw4LjY1OTg3Njg1IEMyNS4zMDEzNDMxLDguNTc4MDQxNjcgMjUuMDYwNzgzMyw4LjUxMDE0MzQ5IDI0Ljc4Mzg4LDguNDU1MTEwMjMgQzI0LjUwNjk3NjcsOC40MDA3OTE2OSAyNC4yMTg5OTcyLDguMzQwNzU1NCAyMy45MTk5NDE2LDguMjc1MzU4NzMgQzIzLjQ5ODcwMjUsOC4xODgxNjMxOCAyMy4wODY0NjI2LDguMDg0ODg2NDcgMjIuNjgyMTgzOCw3Ljk2NDgxMzkgQzIyLjI3NzkwNSw3Ljg0NTA5ODY5IDIxLjkxNTE2MTYsNy42Nzg1Njk0NyAyMS41OTM5NTM4LDcuNDY2Mjk4MzEgQzIxLjI3Mjc0NTksNy4yNTMzMTI0NCAyMS4wMTI0NTY4LDYuOTgwNjQ3NjQgMjAuODEzMDg2NCw2LjY0ODMwMzkyIEMyMC42MTM3MTYsNi4zMTU5NjAyIDIwLjUxNDAzMDgsNS44OTg5MjI0MyAyMC41MTQwMzA4LDUuMzk3NTQ3OTcgQzIwLjUxNDAzMDgsNS4wMTU1MzEzNyAyMC42MDU0MDg5LDQuNjQ3ODA5MTIgMjAuNzg4MTY1MSw0LjI5MzY2NjUgQzIwLjk3MDkyMTMsMy45MzkxNjY1MyAyMS4yMjg0NDE0LDMuNjI2MTIwMTggMjEuNTYwNzI1NCwzLjM1MzA5ODAzIEMyMS44OTMwMDkzLDMuMDgwNzkwNTkgMjIuMjk0MTczLDIuODY1NjYwNTYgMjIuNzY1MjU0OCwyLjcwNzM1MDYgQzIzLjIzNTk5MDQsMi41NDkwNDA2MyAyMy43NTkzMzc3LDIuNDcwMDY0MzIgMjQuMzM1Mjk2NiwyLjQ3MDA2NDMyIFogTTMzLjEwNzM1MTUsMi40NzAwNjQzMiBDMzQuMDgxNzA1LDIuNDcwMDY0MzIgMzQuODkzMzc3OSwyLjY1NjYwNTY0IDM1LjU0MTMzMTYsMy4wMjg5NzM1NiBDMzYuMTg5Mjg1NCwzLjQwMTY5ODgzIDM2LjcwMTU1NjUsMy45MDg0MzM2NyAzNy4wNzgxNDUxLDQuNTUwMjUwMTUgTDM1LjE2NzUxMjIsNS40ODI1OTkzNyBDMzQuOTY4MTQxOCw1LjE0NjMyNDcgMzQuNjkxMjM4NCw0Ljg4MDA5MjM2IDM0LjMzNjgwMjIsNC42ODQ2MTcwOCBDMzMuOTgyMzY1OSw0LjQ4OTE0MTc5IDMzLjU3MjU0OSw0LjM5MTIyNTQ3IDMzLjEwNzM1MTUsNC4zOTEyMjU0NyBDMzIuNTc1Njk3MSw0LjM5MTIyNTQ3IDMyLjE4MjQ5NDQsNC40OTQ4NTk1MyAzMS45Mjc3NDMzLDQuNzAyMTI3NjYgQzMxLjY3MjY0NjEsNC45MDkzOTU3OSAzMS41NDU2MTY3LDUuMTQ5NTQwOTMgMzEuNTQ1NjE2Nyw1LjQyMjIwNTczIEMzMS41NDU2MTY3LDUuNzM4NDY4MzEgMzEuNzMzOTExLDUuOTY1MDMzODEgMzIuMTEwNDk5NSw2LjEwMDgzMDE3IEMzMi40ODY3NDE5LDYuMjM3MzQxMjUgMzMuMDQwODk0Nyw2LjM4MjA3MTU4IDMzLjc3MTkxOTQsNi41MzQ2NjM4MSBDMzQuMTcwNjYwMiw2LjYxMTEzODYgMzQuNTcxODIzOSw2LjcxMTU1NjQ0IDM0Ljk3NjQ0ODksNi44MzczNDY3NSBDMzUuMzgwNzI3Nyw2Ljk2Mjc3OTcgMzUuNzQ5MDA5MSw3LjEzMTgxMDQzIDM2LjA4MTI5MzEsNy4zNDQ3OTYzMSBDMzYuNDEzNTc3MSw3LjU1NzQyNDgyIDM2LjY4MjE3MzMsNy44Mjk3MzIyNiAzNi44ODcwODE4LDguMTYyNzkwNyBDMzcuMDkxOTkwMiw4LjQ5NTQ5MTc4IDM3LjE5NDQ0NDQsOC45MTI1Mjk1NSAzNy4xOTQ0NDQ0LDkuNDE0MjYxMzcgQzM3LjE5NDQ0NDQsOS43NTIzMjI4MyAzNy4xMTY5MTE1LDEwLjEwNDMyMTMgMzYuOTYxODQ1NywxMC40Njk1NDIgQzM2LjgwNjQzMzcsMTAuODM0NzYyOCAzNi41NjU4NzM5LDExLjE3MzE4MTYgMzYuMjM5MTI4LDExLjQ4MzcyNjQgQzM1LjkxMjM4MjEsMTEuNzk0OTg2IDM1LjQ5OTc5NjEsMTIuMDQ4MzUzNCAzNS4wMDEzNzAyLDEyLjI0NDU0MzQgQzM0LjUwMjk0NDIsMTIuNDQxMDkwOCAzMy45MDQ4MzMsMTIuNTM5MDA3MSAzMy4yMDcwMzY3LDEyLjUzOTAwNzEgQzMyLjEzMjY1MTgsMTIuNTM5MDA3MSAzMS4yNDEwMjMxLDEyLjMzMjgxMSAzMC41MzIxNTA2LDExLjkyMDA2MTYgQzI5LjgyMzI3ODEsMTEuNTA2OTU0OCAyOS4yNzUwMDk1LDEwLjkxMDUyMjggMjguODg3MzQ0OSwxMC4xMzAwNTExIEwzMC45NjQxMTk4LDkuMTY1MTgyMjUgQzMxLjE5NjcxODYsOS42MDQ3MzM2MyAzMS41MTIzODgzLDkuOTQyNDM3NzQgMzEuOTExMTI5MSwxMC4xNzg2NTE5IEMzMi4zMDk4Njk5LDEwLjQxNDUwODggMzIuNzc1MDY3NSwxMC41MzIwNzk4IDMzLjMwNjcyMTgsMTAuNTMyMDc5OCBDMzMuODYwNTI4NSwxMC41MzIwNzk4IDM0LjI3NTg4MzUsMTAuNDE3NzI1IDM0LjU1Mjc4NjgsMTAuMTg5MDE1MyBDMzQuODI5MzQ0LDkuOTU5OTQ4MzIgMzQuOTY4MTQxOCw5LjY4NzY0MDg4IDM0Ljk2ODE0MTgsOS4zNzEwMjA5NSBDMzQuOTY4MTQxOCw5LjE5NjYyOTgzIDM0LjkwNDQ1NCw5LjA1NTExNTczIDM0Ljc3NzA3ODUsOC45NDU3NjM5MiBDMzQuNjQ5NzAyOSw4LjgzNzEyNjgzIDM0LjQ4MzU2MSw4Ljc0MTM1NDY2IDM0LjI3ODY1MjUsOC42NTk4NzY4NSBDMzQuMDczMzk3OSw4LjU3ODA0MTY3IDMzLjgzMjgzODIsOC41MTAxNDM0OSAzMy41NTU5MzQ4LDguNDU1MTEwMjMgQzMzLjI3OTAzMTUsOC40MDA3OTE2OSAzMi45OTEwNTIxLDguMzQwNzU1NCAzMi42OTE5OTY1LDguMjc1MzU4NzMgQzMyLjI3MDc1NzMsOC4xODgxNjMxOCAzMS44NTg1MTc1LDguMDg0ODg2NDcgMzEuNDU0MjM4Niw3Ljk2NDgxMzkgQzMxLjA0OTk1OTgsNy44NDUwOTg2OSAzMC42ODcyMTY1LDcuNjc4NTY5NDcgMzAuMzY2MDA4Niw3LjQ2NjI5ODMxIEMzMC4wNDQ4MDA4LDcuMjUzMzEyNDQgMjkuNzg0NTExNiw2Ljk4MDY0NzY0IDI5LjU4NTE0MTIsNi42NDgzMDM5MiBDMjkuMzg1NzcwOSw2LjMxNTk2MDIgMjkuMjg2MDg1Nyw1Ljg5ODkyMjQzIDI5LjI4NjA4NTcsNS4zOTc1NDc5NyBDMjkuMjg2MDg1Nyw1LjAxNTUzMTM3IDI5LjM3NzQ2MzgsNC42NDc4MDkxMiAyOS41NjAyMTk5LDQuMjkzNjY2NSBDMjkuNzQyOTc2MSwzLjkzOTE2NjUzIDMwLjAwMDQ5NjIsMy42MjYxMjAxOCAzMC4zMzI3ODAyLDMuMzUzMDk4MDMgQzMwLjY2NTA2NDIsMy4wODA3OTA1OSAzMS4wNjYyMjc5LDIuODY1NjYwNTYgMzEuNTM3MzA5NiwyLjcwNzM1MDYgQzMyLjAwODA0NTMsMi41NDkwNDA2MyAzMi41MzEzOTI2LDIuNDcwMDY0MzIgMzMuMTA3MzUxNSwyLjQ3MDA2NDMyIFogTTEzLjc3MzIwNTcsMi40NzAwMjg1OSBDMTQuNDE1NjIxNCwyLjQ3MDAyODU5IDE1LjAwODE5NDUsMi41OTAxMDExNiAxNS41NTA5MjUsMi44Mjk1MzE1OSBDMTYuMDkzMzA5NCwzLjA2OTY3NjczIDE2LjU0MjIzODksMy4zOTY2NjAwNyAxNi44OTY2NzUxLDMuODEwODM4OTcgTDE2Ljg5NjY3NTEsMi40ODcxODE4MSBMMTkuMTM5NTkyLDIuNDg3MTgxODEgTDE5LjEzOTU5MiwxMi41MjE4MTgxIEwxNi44OTY2NzUxLDEyLjUyMTgxODEgTDE2Ljg5NjY3NTEsMTEuMDk1OTU2MyBDMTYuNTQyMjM4OSwxMS41NDQwODQzIDE2LjA4ODExNzQsMTEuODk2Nzk3NSAxNS41MzQzMTA4LDEyLjE1MzczODUgQzE0Ljk4MDUwNDIsMTIuNDEwNjc5NSAxNC4zODIzOTMsMTIuNTM4OTcxNCAxMy43Mzk5NzczLDEyLjUzODk3MTQgQzEzLjE1Mjk0MjMsMTIuNTM4OTcxNCAxMi41NzQyMTQzLDEyLjQyNjQwMzMgMTIuMDAzNzkzNSwxMi4yMDA1NTI1IEMxMS40MzMzNzI2LDExLjk3NDM0NDQgMTAuOTIxMTAxNSwxMS42NDYyODkgMTAuNDY2OTgwMSwxMS4yMTYzODYzIEMxMC4wMTI4NTg2LDEwLjc4NjQ4MzYgOS42NDcwMDAxMSwxMC4yNjAwOTQgOS4zNzA0NDI5Miw5LjYzNzU3NDkxIEM5LjA5MzUzOTYsOS4wMTQ2OTg0NCA4Ljk1NTA4Nzk0LDguMzA2NDEzMjIgOC45NTUwODc5NCw3LjUxMzA3NjU4IEM4Ljk1NTA4Nzk0LDYuNzA4MzA0NDcgOS4wOTA0MjQ0NCw1Ljk5NTAxNjIyIDkuMzYyMTM1ODIsNS4zNzE3ODI0IEM5LjYzMzUwMTA3LDQuNzQ4OTA1OTMgOS45OTM0NzUzOSw0LjIyMjg3MzcxIDEwLjQ0MjA1ODgsMy43OTI5NzEwMyBDMTAuODkwNjQyMSwzLjM2MjcxMDk4IDExLjQwNTY4MjMsMy4wMzUwMTI5MiAxMS45ODcxNzkzLDIuODA5NTE5NDkgQzEyLjU2ODY3NjMsMi41ODMzMTEzNCAxMy4xNjQwMTg0LDIuNDcwMDI4NTkgMTMuNzczMjA1NywyLjQ3MDAyODU5IFogTTQuMTUzNTQ5NzgsMCBDNC43ODQ4ODkzNSwwIDUuMzY2Mzg2MzIsMC4xMTcyMTM3MDEgNS44OTgwNDA2OSwwLjM1MTk5ODQ2MSBDNi40Mjk2OTUwNiwwLjU4NjQyNTg2MiA2Ljg4OTAwODQ0LDAuOTAzNDAzMTU2IDcuMjc3MDE5MjIsMS4zMDQwMDI0MiBDNy42NjQ2ODM4NiwxLjcwNDI0NDMyIDcuOTY4OTMxMzgsMi4xNzU5NTggOC4xOTA4MDAxNywyLjcxOTE0MzQ0IEM4LjQxMjMyMjgyLDMuMjYxOTcxNTIgOC41MjMwODQxNSwzLjg0MjMyMjI4IDguNTIzMDg0MTUsNC40NjAxOTU3MiBDOC41MjMwODQxNSw1LjA3NzM1NDQ0IDguNDEyMzIyODIsNS42NjA1NjQwOCA4LjE5MDgwMDE3LDYuMjA5NDY3MjYgQzcuOTY4OTMxMzgsNi43NTgzNzA0NCA3LjY2NDY4Mzg2LDcuMjMyOTQyOTkgNy4yNzcwMTkyMiw3LjYzMzU0MjI1IEM2Ljg4OTAwODQ0LDguMDMzNzg0MTYgNi40MjY5MjYwMyw4LjM1MTExODgxIDUuODg5NzMzNTksOC41ODUxODg4NSBDNS4zNTIxOTUwMiw4LjgxOTYxNjI1IDQuNzY4Mjc1MTUsOC45MzY4Mjk5NSA0LjEzNjkzNTU4LDguOTM2ODI5OTUgTDIuMjU5NTMxMDgsOC45MzY4Mjk5NSBMMi4yNTk1MzEwOCwxMi41MjE4NTM5IEwwLDEyLjUyMTg1MzkgTDAsMCBMNC4xNTM1NDk3OCwwIFogTTE0LjEwNTQ4OTcsNC41ODAyMzI1NiBDMTMuNjk1NjcyOCw0LjU4MDIzMjU2IDEzLjMxMDc3NzEsNC42NTU2MzUyNyAxMi45NTA4MDI4LDQuODA1NzI1OTkgQzEyLjU5MDgyODUsNC45NTU4MTY3IDEyLjI3NzkyNzgsNS4xNjAyMjU5NiAxMi4wMTIxMDA2LDUuNDE3NTI0MzMgQzExLjc0NjI3MzQsNS42NzU1Mzc0MSAxMS41Mzg1OTU5LDUuOTgxNzkzOTQgMTEuMzg5MDY4MSw2LjMzNjI5MzkxIEMxMS4yMzk1NDAzLDYuNjkwNzkzODkgMTEuMTY0Nzc2NCw3LjA3MTczODQxIDExLjE2NDc3NjQsNy40ODAxOTk1NyBDMTEuMTY0Nzc2NCw3Ljg4ODMwMzM3IDExLjIzOTU0MDMsOC4yNzI0NjQxMyAxMS4zODkwNjgxLDguNjMxOTY3MTIgQzExLjUzODU5NTksOC45OTE0NzAxMiAxMS43NDYyNzM0LDkuMzAyNzI5NjcgMTIuMDEyMTAwNiw5LjU2NjQ2MDUgQzEyLjI3NzkyNzgsOS44Mjk0NzY2MSAxMi41OTA4Mjg1LDEwLjAzNjAzIDEyLjk1MDgwMjgsMTAuMTg2ODM1NCBDMTMuMzEwNzc3MSwxMC4zMzY5MjYyIDEzLjY5NTY3MjgsMTAuNDExOTcxNSAxNC4xMDU0ODk3LDEwLjQxMTk3MTUgQzE0LjUyNjM4MjcsMTAuNDExOTcxNSAxNC45MTM3MDEyLDEwLjMzNjkyNjIgMTUuMjY4NDgzNiwxMC4xODY4MzU0IEMxNS42MjI5MTk5LDEwLjAzNjAzIDE1LjkyNzE2NzQsOS44MjY2MTc3NCAxNi4xODIyNjQ2LDkuNTU3ODgzODkgQzE2LjQzNzAxNTYsOS4yODk1MDczOSAxNi42MzkxNTUsOC45Nzc4OTA0OCAxNi43ODg2ODI4LDguNjIzNzQ3ODcgQzE2LjkzODIxMDYsOC4yNjk2MDUyNiAxNy4wMTI5NzQ1LDcuODg4MzAzMzcgMTcuMDEyOTc0NSw3LjQ4MDE5OTU3IEMxNy4wMTI5NzQ1LDcuMDgyODE2NTQgMTYuOTM4MjEwNiw2LjcwNjg3NTAzIDE2Ljc4ODY4MjgsNi4zNTIzNzUwNiBDMTYuNjM5MTU1LDUuOTk3ODc1MDkgMTYuNDM3MDE1Niw1LjY4OTExNzA1IDE2LjE4MjI2NDYsNS40MjYxMDA5NCBDMTUuOTI3MTY3NCw1LjE2MjcyNzQ3IDE1LjYyMjkxOTksNC45NTU4MTY3IDE1LjI2ODQ4MzYsNC44MDU3MjU5OSBDMTQuOTEzNzAxMiw0LjY1NTYzNTI3IDE0LjUyNjM4MjcsNC41ODAyMzI1NiAxNC4xMDU0ODk3LDQuNTgwMjMyNTYgWiBNMy45ODc0MDc3OSwyLjE2MTMwNjI4IEwyLjI1OTUzMTA4LDIuMTYxMzA2MjggTDIuMjU5NTMxMDgsNi43NzU1MjM2NyBMMy45ODc0MDc3OSw2Ljc3NTUyMzY3IEM0LjMxOTY5MTc3LDYuNzc1NTIzNjcgNC42MjQyODU0Miw2LjcxNTQ4NzM4IDQuOTAxMTg4NzQsNi41OTU0MTQ4MSBDNS4xNzc3NDU5Myw2LjQ3NTM0MjI0IDUuNDE2MjI4OTIsNi4zMDk1Mjc3NCA1LjYxNTU5OTMsNi4wOTgzMjg2NiBDNS44MTQ5Njk2OSw1Ljg4Njc3MjIyIDUuOTcwMDM1NTUsNS42NDA1NTE5OCA2LjA4MDc5Njg4LDUuMzYwMzgyNjUgQzYuMTkxMjEyMDgsNS4wODA1NzA2NyA2LjI0NjkzODg3LDQuNzgwMDMxODkgNi4yNDY5Mzg4Nyw0LjQ2MDE5NTcyIEM2LjI0NjkzODg3LDQuMTM5NjQ0ODQgNi4xOTEyMTIwOCwzLjgzOTQ2MzQxIDYuMDgwNzk2ODgsMy41NTkyOTQwOCBDNS45NzAwMzU1NSwzLjI3OTEyNDc1IDUuODE0OTY5NjksMy4wMzYxMjA3MyA1LjYxNTU5OTMsMi44MzAyODIwNCBDNS40MTYyMjg5MiwyLjYyNDQ0MzM0IDUuMTc3NzQ1OTMsMi40NjE0ODc3MSA0LjkwMTE4ODc0LDIuMzQxNDE1MTQgQzQuNjI0Mjg1NDIsMi4yMjEzNDI1NyA0LjMxOTY5MTc3LDIuMTYxMzA2MjggMy45ODc0MDc3OSwyLjE2MTMwNjI4IFoiPjwvcGF0aD48L2NsaXBQYXRoPjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjMzLjAgLTE2MC4wKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2kwKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjMzLjAgMTYwLjApIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMC4wMDAxODA1NTU1NTU1NTIwNzU0OCAwLjApIj48ZyBjbGlwLXBhdGg9InVybCgjaTEpIj48cG9seWdvbiBwb2ludHM9IjAsMCA1MiwwIDUyLDUyIDAsNTIgMCwwIiBzdHJva2U9Im5vbmUiIGZpbGw9InVybCgjaTIpIj48L3BvbHlnb24+PC9nPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3LjU4MzMzMzMzMzMzMzMzMiAxNC44MDU1NTU1NTU1NTU1NSkiPjxnIGNsaXAtcGF0aD0idXJsKCNpMykiPjxnIGNsaXAtcGF0aD0idXJsKCNpNCkiPjxwb2x5Z29uIHBvaW50cz0iMCwwIDM2LjQ3OTE2NjcsMCAzNi40NzkxNjY3LDUuNjEyMTc5NDkgMCw1LjYxMjE3OTQ5IDAsMCIgc3Ryb2tlPSJub25lIiBmaWxsPSIjRkZGRkZGIj48L3BvbHlnb24+PC9nPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNy41ODMzMzMzMzMzMzMzNzEgMjQuNDMyMDgwMjU1NDc3MzMpIj48ZyBjbGlwLXBhdGg9InVybCgjaTUpIj48cG9seWdvbiBwb2ludHM9IjAsMCAzNy4xOTQ0NDQ0LDAgMzcuMTk0NDQ0NCwxMi41MzkwMDcxIDAsMTIuNTM5MDA3MSAwLDAiIHN0cm9rZT0ibm9uZSIgZmlsbD0iI0ZGRkZGRiI+PC9wb2x5Z29uPjwvZz48L2c+PC9nPjwvZz48L2c+PC9zdmc+" + }, + "66a0ccb3-bd6a-191f-ee06-e375c50b9846": { + "name": "Thales Bio iOS SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + }, + "8836336a-f590-0921-301d-46427531eee6": { + "name": "Thales Bio Android SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + }, + "cd69adb5-3c7a-deb9-3177-6800ea6cb72a": { + "name": "Thales PIN Android SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + }, + "17290f1e-c212-34d0-1423-365d729f09d9": { + "name": "Thales PIN iOS SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + } +} \ No newline at end of file diff --git a/selfservice/strategy/webauthn/errors.go b/x/webauthnx/errors.go similarity index 95% rename from selfservice/strategy/webauthn/errors.go rename to x/webauthnx/errors.go index 882a436ed179..f6347c35ddbc 100644 --- a/selfservice/strategy/webauthn/errors.go +++ b/x/webauthnx/errors.go @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -package webauthn +package webauthnx import ( "github.com/pkg/errors" @@ -9,6 +9,7 @@ import ( "github.com/ory/jsonschema/v3" ) +var ErrNoCredentials = errors.New("required credentials not found") + var ErrNotEnoughCredentials = &jsonschema.ValidationError{ Message: "unable to remove this security key because it would lock you out of your account", InstancePtr: "#/webauthn_remove"} -var ErrNoCredentials = errors.New("required credentials not found") diff --git a/selfservice/strategy/webauthn/handler.go b/x/webauthnx/handler.go similarity index 74% rename from selfservice/strategy/webauthn/handler.go rename to x/webauthnx/handler.go index 7036b5bdf01b..1d71419ee193 100644 --- a/selfservice/strategy/webauthn/handler.go +++ b/x/webauthnx/handler.go @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -package webauthn +package webauthnx import ( _ "embed" @@ -15,9 +15,12 @@ import ( //go:embed js/webauthn.js var jsOnLoad []byte -const webAuthnRoute = "/.well-known/ory/webauthn.js" +const ScriptURL = "/.well-known/ory/webauthn.js" // swagger:model webAuthnJavaScript +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type webAuthnJavaScript string // swagger:route GET /.well-known/ory/webauthn.js frontend getWebAuthnJavaScript @@ -41,11 +44,11 @@ type webAuthnJavaScript string // // Responses: // 200: webAuthnJavaScript -func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { - if handle, _, _ := r.Lookup("GET", webAuthnRoute); handle == nil { - r.GET(webAuthnRoute, func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func RegisterWebauthnRoute(r *x.RouterPublic) { + if handle, _, _ := r.Lookup("GET", ScriptURL); handle == nil { + r.GET(ScriptURL, func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "text/javascript; charset=UTF-8") - _, _ = w.Write([]byte(webAuthnJavaScript(jsOnLoad))) + _, _ = w.Write(jsOnLoad) }) } } diff --git a/x/webauthnx/js/webauthn.js b/x/webauthnx/js/webauthn.js new file mode 100644 index 000000000000..61a7cb8f976d --- /dev/null +++ b/x/webauthnx/js/webauthn.js @@ -0,0 +1,380 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +;(function () { + if (!window) { + return + } + + if (!window.PublicKeyCredential) { + console.log("This browser does not support WebAuthn!") + return + } + + if (window.__oryWebAuthnInitialized) { + return + } + + function __oryWebAuthnBufferDecode(value) { + return Uint8Array.from( + atob(value.replaceAll("-", "+").replaceAll("_", "/")), + function (c) { + return c.charCodeAt(0) + }, + ) + } + + function __oryWebAuthnBufferEncode(value) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(value))) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replaceAll("=", "") + } + + function __oryWebAuthnLogin( + opt, + resultQuerySelector = '*[name="webauthn_login"]', + triggerQuerySelector = '*[name="webauthn_login_trigger"]', + ) { + if (!window.PublicKeyCredential) { + alert("This browser does not support WebAuthn!") + } + + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + + navigator.credentials + .get(opt) + .then(function (credential) { + document.querySelector(resultQuerySelector).value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + authenticatorData: __oryWebAuthnBufferEncode( + credential.response.authenticatorData, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + signature: __oryWebAuthnBufferEncode(credential.response.signature), + userHandle: __oryWebAuthnBufferEncode( + credential.response.userHandle, + ), + }, + }) + + document.querySelector(triggerQuerySelector).closest("form").submit() + }) + .catch((err) => { + alert(err) + }) + } + + function __oryWebAuthnRegistration( + opt, + resultQuerySelector = '*[name="webauthn_register"]', + triggerQuerySelector = '*[name="webauthn_register_trigger"]', + ) { + if (!window.PublicKeyCredential) { + alert("This browser does not support WebAuthn!") + } + + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + + if (opt.publicKey.excludeCredentials) { + opt.publicKey.excludeCredentials = opt.publicKey.excludeCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + } + + navigator.credentials + .create(opt) + .then(function (credential) { + document.querySelector(resultQuerySelector).value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + attestationObject: __oryWebAuthnBufferEncode( + credential.response.attestationObject, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + }, + }) + + document.querySelector(triggerQuerySelector).closest("form").submit() + }) + .catch((err) => { + alert(err) + }) + } + + window.__oryPasskeyLoginAutocompleteInit = async function () { + const dataEl = document.getElementsByName("passkey_challenge")[0] + const resultEl = document.getElementsByName("passkey_login")[0] + const identifierEl = document.getElementsByName("identifier")[0] + + if (!dataEl || !resultEl || !identifierEl) { + console.debug( + "__oryPasskeyLoginAutocompleteInit: mandatory fields not found", + ) + return + } + + if ( + !window.PublicKeyCredential || + !window.PublicKeyCredential.isConditionalMediationAvailable || + window.Cypress // Cypress auto-fills the autocomplete, which we don't want + ) { + console.log("This browser does not support WebAuthn!") + return + } + const isCMA = await PublicKeyCredential.isConditionalMediationAvailable() + if (!isCMA) { + console.log( + "This browser does not support WebAuthn Conditional Mediation!", + ) + return + } + + let opt = JSON.parse(dataEl.value) + + if (opt.publicKey.user && opt.publicKey.user.id) { + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + } + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + + // Allow aborting through a global variable + window.abortPasskeyConditionalUI = new AbortController() + + navigator.credentials + .get({ + publicKey: opt.publicKey, + mediation: "conditional", + signal: abortPasskeyConditionalUI.signal, + }) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + authenticatorData: __oryWebAuthnBufferEncode( + credential.response.authenticatorData, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + signature: __oryWebAuthnBufferEncode(credential.response.signature), + userHandle: __oryWebAuthnBufferEncode( + credential.response.userHandle, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + console.log(err) + }) + } + + window.__oryPasskeyLogin = function () { + const dataEl = document.getElementsByName("passkey_challenge")[0] + const resultEl = document.getElementsByName("passkey_login")[0] + + if (!dataEl || !resultEl) { + console.debug("__oryPasskeyLogin: mandatory fields not found") + return + } + if (!window.PublicKeyCredential) { + console.log("This browser does not support WebAuthn!") + return + } + + let opt = JSON.parse(dataEl.value) + + if (opt.publicKey.user && opt.publicKey.user.id) { + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + } + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + if (opt.publicKey.allowCredentials) { + opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( + function (cred) { + return { + ...cred, + id: __oryWebAuthnBufferDecode(cred.id), + } + }, + ) + } + + window.abortPasskeyConditionalUI && + window.abortPasskeyConditionalUI.abort( + "only one credentials.get allowed at a time", + ) + + navigator.credentials + .get({ + publicKey: opt.publicKey, + }) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + authenticatorData: __oryWebAuthnBufferEncode( + credential.response.authenticatorData, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + signature: __oryWebAuthnBufferEncode(credential.response.signature), + userHandle: __oryWebAuthnBufferEncode( + credential.response.userHandle, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + // Calling this again will enable the autocomplete once again. + console.error(err) + window.abortPasskeyConditionalUI && __oryPasskeyLoginAutocompleteInit() + }) + } + + window.__oryPasskeyRegistration = function () { + const dataEl = document.getElementsByName("passkey_create_data")[0] + const resultEl = document.getElementsByName("passkey_register")[0] + + if (!dataEl || !resultEl) { + console.debug("__oryPasskeyRegistration: mandatory fields not found") + return + } + + const createData = JSON.parse(dataEl.value) + + // Fetch display name from field value + const displayNameFieldName = createData.displayNameFieldName + const displayName = dataEl + .closest("form") + .querySelector("[name='" + displayNameFieldName + "']").value + + let opts = createData.credentialOptions + opts.publicKey.user.name = displayName + opts.publicKey.user.displayName = displayName + opts.publicKey.user.id = __oryWebAuthnBufferDecode(opts.publicKey.user.id) + opts.publicKey.challenge = __oryWebAuthnBufferDecode( + opts.publicKey.challenge, + ) + + if (opts.publicKey.excludeCredentials) { + opts.publicKey.excludeCredentials = opts.publicKey.excludeCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + } + + navigator.credentials + .create(opts) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + attestationObject: __oryWebAuthnBufferEncode( + credential.response.attestationObject, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + console.error(err) + }) + } + + function __oryPasskeySettingsRegistration() { + const dataEl = document.getElementsByName("passkey_create_data")[0] + const resultEl = document.getElementsByName("passkey_settings_register")[0] + + if (!dataEl || !resultEl) { + console.debug( + "__oryPasskeySettingsRegistration: mandatory fields not found", + ) + return + } + + let opt = JSON.parse(dataEl.value) + + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + + if (opt.publicKey.excludeCredentials) { + opt.publicKey.excludeCredentials = opt.publicKey.excludeCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + } + + navigator.credentials + .create(opt) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + attestationObject: __oryWebAuthnBufferEncode( + credential.response.attestationObject, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + console.error(err) + }) + } + + window.__oryWebAuthnLogin = __oryWebAuthnLogin + window.__oryWebAuthnRegistration = __oryWebAuthnRegistration + window.__oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration + window.__oryWebAuthnInitialized = true +})() diff --git a/selfservice/strategy/webauthn/nodes.go b/x/webauthnx/nodes.go similarity index 58% rename from selfservice/strategy/webauthn/nodes.go rename to x/webauthnx/nodes.go index cfbffa4be428..76fac1c397cd 100644 --- a/selfservice/strategy/webauthn/nodes.go +++ b/x/webauthnx/nodes.go @@ -1,15 +1,17 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -package webauthn +package webauthnx import ( "crypto/sha512" _ "embed" "encoding/base64" "fmt" + "net/url" "github.com/ory/x/stringsx" + "github.com/ory/x/urlx" "github.com/ory/kratos/identity" "github.com/ory/kratos/text" @@ -23,9 +25,15 @@ func NewWebAuthnConnectionTrigger(options string) *node.Node { })) } -func NewWebAuthnScript(src string, contents []byte) *node.Node { - integrity := sha512.Sum512(contents) - return node.NewScriptField(node.WebAuthnScript, src, node.WebAuthnGroup, fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(integrity[:]))) +func NewWebAuthnScript(base *url.URL) *node.Node { + src := urlx.AppendPaths(base, ScriptURL).String() + integrity := sha512.Sum512(jsOnLoad) + return node.NewScriptField( + node.WebAuthnScript, + src, + node.WebAuthnGroup, + fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(integrity[:])), + ) } func NewWebAuthnConnectionInput() *node.Node { @@ -50,8 +58,22 @@ func NewWebAuthnConnectionName() *node.Node { WithMetaLabel(text.NewInfoSelfServiceRegisterWebAuthnDisplayName()) } -func NewWebAuthnUnlink(c *identity.CredentialWebAuthn) *node.Node { - return node.NewInputField(node.WebAuthnRemove, fmt.Sprintf("%x", c.ID), node.WebAuthnGroup, - node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoSelfServiceRemoveWebAuthn(stringsx.Coalesce(c.DisplayName, "unnamed"), c.AddedAt)) +func NewWebAuthnUnlink(c *identity.CredentialWebAuthn, opts ...node.InputAttributesModifier) *node.Node { + return node.NewInputField( + node.WebAuthnRemove, + fmt.Sprintf("%x", c.ID), + node.WebAuthnGroup, + node.InputAttributeTypeSubmit, + opts..., + ).WithMetaLabel(text.NewInfoSelfServiceRemoveWebAuthn(stringsx.Coalesce(c.DisplayName, "unnamed"), c.AddedAt)) +} + +func NewPasskeyUnlink(c *identity.CredentialWebAuthn, opts ...node.InputAttributesModifier) *node.Node { + return node.NewInputField( + node.PasskeyRemove, + fmt.Sprintf("%x", c.ID), + node.PasskeyGroup, + node.InputAttributeTypeSubmit, + opts..., + ).WithMetaLabel(text.NewInfoSelfServiceRemovePasskey(stringsx.Coalesce(c.DisplayName, "unnamed"), c.AddedAt)) } diff --git a/x/webauthnx/user.go b/x/webauthnx/user.go new file mode 100644 index 000000000000..bc369f6851ab --- /dev/null +++ b/x/webauthnx/user.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthnx + +import ( + "github.com/go-webauthn/webauthn/webauthn" + + "github.com/ory/x/stringsx" +) + +var _ webauthn.User = (*User)(nil) + +type User struct { + Name string + ID []byte + Credentials []webauthn.Credential + Config *webauthn.Config +} + +func NewUser(id []byte, credentials []webauthn.Credential, config *webauthn.Config) *User { + return &User{ + ID: id, + Credentials: credentials, + Config: config, + } +} + +func (u *User) WebAuthnID() []byte { + return u.ID +} + +func (u *User) WebAuthnName() string { + return stringsx.Coalesce(u.Name, u.Config.RPDisplayName) +} + +func (u *User) WebAuthnDisplayName() string { + return stringsx.Coalesce(u.Name, u.Config.RPDisplayName) +} + +func (u *User) WebAuthnIcon() string { + return "" // Icon option has been removed due to security considerations. +} + +func (u *User) WebAuthnCredentials() []webauthn.Credential { + return u.Credentials +} From b47554b154db9d3edb0043459607a82d7869d37d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:32:49 +0000 Subject: [PATCH 035/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/.openapi-generator/FILES | 2 + internal/client-go/README.md | 1 + internal/client-go/api_identity.go | 4 +- internal/client-go/go.sum | 1 - .../client-go/model_identity_credentials.go | 2 +- internal/client-go/model_login_flow.go | 2 +- internal/client-go/model_registration_flow.go | 2 +- ...e_registration_flow_with_profile_method.go | 249 ++++++++++++++++++ internal/httpclient/.openapi-generator/FILES | 2 + internal/httpclient/README.md | 1 + internal/httpclient/api_identity.go | 4 +- .../httpclient/model_identity_credentials.go | 2 +- internal/httpclient/model_login_flow.go | 2 +- .../httpclient/model_registration_flow.go | 2 +- ...e_registration_flow_with_profile_method.go | 249 ++++++++++++++++++ spec/api.json | 56 +++- spec/swagger.json | 62 ++++- 17 files changed, 614 insertions(+), 29 deletions(-) create mode 100644 internal/client-go/model_update_registration_flow_with_profile_method.go create mode 100644 internal/httpclient/model_update_registration_flow_with_profile_method.go diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index 4085540e8053..fdf34c5e1507 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -113,6 +113,7 @@ docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md docs/UpdateRegistrationFlowWithPasskeyMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md +docs/UpdateRegistrationFlowWithProfileMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md docs/UpdateSettingsFlowBody.md docs/UpdateSettingsFlowWithLookupMethod.md @@ -232,6 +233,7 @@ model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go model_update_registration_flow_with_passkey_method.go model_update_registration_flow_with_password_method.go +model_update_registration_flow_with_profile_method.go model_update_registration_flow_with_web_authn_method.go model_update_settings_flow_body.go model_update_settings_flow_with_lookup_method.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index 345c54129d58..33082914bfef 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -236,6 +236,7 @@ Class | Method | HTTP request | Description - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) + - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index 97354d5c0d0b..c898733ecd4b 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -114,7 +114,7 @@ type IdentityApi interface { You can only delete second factor (aal2) credentials. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode * @return IdentityApiApiDeleteIdentityCredentialsRequest */ DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest @@ -1077,7 +1077,7 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons You can only delete second factor (aal2) credentials. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - @return IdentityApiApiDeleteIdentityCredentialsRequest */ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_identity_credentials.go b/internal/client-go/model_identity_credentials.go index 5b454383d520..7ee96800df4b 100644 --- a/internal/client-go/model_identity_credentials.go +++ b/internal/client-go/model_identity_credentials.go @@ -23,7 +23,7 @@ type IdentityCredentials struct { CreatedAt *time.Time `json:"created_at,omitempty"` // Identifiers represents a list of unique identifiers this credential type matches. Identifiers []string `json:"identifiers,omitempty"` - // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Type *string `json:"type,omitempty"` // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt *time.Time `json:"updated_at,omitempty"` diff --git a/internal/client-go/model_login_flow.go b/internal/client-go/model_login_flow.go index 777e9b8a7316..2794adee0b83 100644 --- a/internal/client-go/model_login_flow.go +++ b/internal/client-go/model_login_flow.go @@ -18,7 +18,7 @@ import ( // LoginFlow This object represents a login flow. A login flow is initiated at the \"Initiate Login API / Browser Flow\" endpoint by a client. Once a login flow is completed successfully, a session cookie or session token will be issued. type LoginFlow struct { - // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // CreatedAt is a helper struct field for gobuffalo.pop. CreatedAt *time.Time `json:"created_at,omitempty"` diff --git a/internal/client-go/model_registration_flow.go b/internal/client-go/model_registration_flow.go index 71ab2c03ebe2..c0ba64843d3f 100644 --- a/internal/client-go/model_registration_flow.go +++ b/internal/client-go/model_registration_flow.go @@ -18,7 +18,7 @@ import ( // RegistrationFlow struct for RegistrationFlow type RegistrationFlow struct { - // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in, a new flow has to be initiated. ExpiresAt time.Time `json:"expires_at"` diff --git a/internal/client-go/model_update_registration_flow_with_profile_method.go b/internal/client-go/model_update_registration_flow_with_profile_method.go new file mode 100644 index 000000000000..221e5ea82ada --- /dev/null +++ b/internal/client-go/model_update_registration_flow_with_profile_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithProfileMethod Update Registration Flow with Profile Method +type UpdateRegistrationFlowWithProfileMethod struct { + // The Anti-CSRF Token This token is only required when performing browser flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to profile when trying to update a profile. + Method string `json:"method"` + // Screen requests navigation to a previous screen. This must be set to credential-selection to go back to the credential selection screen. + Screen *string `json:"screen,omitempty"` + // Traits The identity's traits. + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithProfileMethod instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithProfileMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithProfileMethodWithDefaults instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithProfileMethodWithDefaults() *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetMethod(v string) { + o.Method = v +} + +// GetScreen returns the Screen field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreen() string { + if o == nil || o.Screen == nil { + var ret string + return ret + } + return *o.Screen +} + +// GetScreenOk returns a tuple with the Screen field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreenOk() (*string, bool) { + if o == nil || o.Screen == nil { + return nil, false + } + return o.Screen, true +} + +// HasScreen returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasScreen() bool { + if o != nil && o.Screen != nil { + return true + } + + return false +} + +// SetScreen gets a reference to the given string and assigns it to the Screen field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetScreen(v string) { + o.Screen = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Screen != nil { + toSerialize["screen"] = o.Screen + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithProfileMethod struct { + value *UpdateRegistrationFlowWithProfileMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) Get() *UpdateRegistrationFlowWithProfileMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Set(val *UpdateRegistrationFlowWithProfileMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithProfileMethod(val *UpdateRegistrationFlowWithProfileMethod) *NullableUpdateRegistrationFlowWithProfileMethod { + return &NullableUpdateRegistrationFlowWithProfileMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index 4085540e8053..fdf34c5e1507 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -113,6 +113,7 @@ docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md docs/UpdateRegistrationFlowWithPasskeyMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md +docs/UpdateRegistrationFlowWithProfileMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md docs/UpdateSettingsFlowBody.md docs/UpdateSettingsFlowWithLookupMethod.md @@ -232,6 +233,7 @@ model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go model_update_registration_flow_with_passkey_method.go model_update_registration_flow_with_password_method.go +model_update_registration_flow_with_profile_method.go model_update_registration_flow_with_web_authn_method.go model_update_settings_flow_body.go model_update_settings_flow_with_lookup_method.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 345c54129d58..33082914bfef 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -236,6 +236,7 @@ Class | Method | HTTP request | Description - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) + - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index 97354d5c0d0b..c898733ecd4b 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -114,7 +114,7 @@ type IdentityApi interface { You can only delete second factor (aal2) credentials. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode * @return IdentityApiApiDeleteIdentityCredentialsRequest */ DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest @@ -1077,7 +1077,7 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons You can only delete second factor (aal2) credentials. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - @return IdentityApiApiDeleteIdentityCredentialsRequest */ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { diff --git a/internal/httpclient/model_identity_credentials.go b/internal/httpclient/model_identity_credentials.go index 5b454383d520..7ee96800df4b 100644 --- a/internal/httpclient/model_identity_credentials.go +++ b/internal/httpclient/model_identity_credentials.go @@ -23,7 +23,7 @@ type IdentityCredentials struct { CreatedAt *time.Time `json:"created_at,omitempty"` // Identifiers represents a list of unique identifiers this credential type matches. Identifiers []string `json:"identifiers,omitempty"` - // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Type *string `json:"type,omitempty"` // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt *time.Time `json:"updated_at,omitempty"` diff --git a/internal/httpclient/model_login_flow.go b/internal/httpclient/model_login_flow.go index 777e9b8a7316..2794adee0b83 100644 --- a/internal/httpclient/model_login_flow.go +++ b/internal/httpclient/model_login_flow.go @@ -18,7 +18,7 @@ import ( // LoginFlow This object represents a login flow. A login flow is initiated at the \"Initiate Login API / Browser Flow\" endpoint by a client. Once a login flow is completed successfully, a session cookie or session token will be issued. type LoginFlow struct { - // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // CreatedAt is a helper struct field for gobuffalo.pop. CreatedAt *time.Time `json:"created_at,omitempty"` diff --git a/internal/httpclient/model_registration_flow.go b/internal/httpclient/model_registration_flow.go index 71ab2c03ebe2..c0ba64843d3f 100644 --- a/internal/httpclient/model_registration_flow.go +++ b/internal/httpclient/model_registration_flow.go @@ -18,7 +18,7 @@ import ( // RegistrationFlow struct for RegistrationFlow type RegistrationFlow struct { - // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in, a new flow has to be initiated. ExpiresAt time.Time `json:"expires_at"` diff --git a/internal/httpclient/model_update_registration_flow_with_profile_method.go b/internal/httpclient/model_update_registration_flow_with_profile_method.go new file mode 100644 index 000000000000..221e5ea82ada --- /dev/null +++ b/internal/httpclient/model_update_registration_flow_with_profile_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithProfileMethod Update Registration Flow with Profile Method +type UpdateRegistrationFlowWithProfileMethod struct { + // The Anti-CSRF Token This token is only required when performing browser flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to profile when trying to update a profile. + Method string `json:"method"` + // Screen requests navigation to a previous screen. This must be set to credential-selection to go back to the credential selection screen. + Screen *string `json:"screen,omitempty"` + // Traits The identity's traits. + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithProfileMethod instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithProfileMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithProfileMethodWithDefaults instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithProfileMethodWithDefaults() *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetMethod(v string) { + o.Method = v +} + +// GetScreen returns the Screen field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreen() string { + if o == nil || o.Screen == nil { + var ret string + return ret + } + return *o.Screen +} + +// GetScreenOk returns a tuple with the Screen field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreenOk() (*string, bool) { + if o == nil || o.Screen == nil { + return nil, false + } + return o.Screen, true +} + +// HasScreen returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasScreen() bool { + if o != nil && o.Screen != nil { + return true + } + + return false +} + +// SetScreen gets a reference to the given string and assigns it to the Screen field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetScreen(v string) { + o.Screen = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Screen != nil { + toSerialize["screen"] = o.Screen + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithProfileMethod struct { + value *UpdateRegistrationFlowWithProfileMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) Get() *UpdateRegistrationFlowWithProfileMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Set(val *UpdateRegistrationFlowWithProfileMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithProfileMethod(val *UpdateRegistrationFlowWithProfileMethod) *NullableUpdateRegistrationFlowWithProfileMethod { + return &NullableUpdateRegistrationFlowWithProfileMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/spec/api.json b/spec/api.json index 2910ad7c6999..4a35eaeb87bc 100644 --- a/spec/api.json +++ b/spec/api.json @@ -992,7 +992,7 @@ "type": "array" }, "type": { - "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "enum": [ "password", "oidc", @@ -1000,11 +1000,13 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "updated_at": { "description": "UpdatedAt is a helper struct field for gobuffalo.pop.", @@ -1265,7 +1267,7 @@ "description": "This object represents a login flow. A login flow is initiated at the \"Initiate Login API / Browser Flow\"\nendpoint by a client.\n\nOnce a login flow is completed successfully, a session cookie or session token will be issued.", "properties": { "active": { - "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "enum": [ "password", "oidc", @@ -1273,11 +1275,13 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "created_at": { "description": "CreatedAt is a helper struct field for gobuffalo.pop.", @@ -1717,7 +1721,7 @@ "registrationFlow": { "properties": { "active": { - "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "enum": [ "password", "oidc", @@ -1725,11 +1729,13 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "expires_at": { "description": "ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in,\na new flow has to be initiated.", @@ -3024,6 +3030,36 @@ ], "type": "object" }, + "updateRegistrationFlowWithProfileMethod": { + "description": "Update Registration Flow with Profile Method", + "properties": { + "csrf_token": { + "description": "The Anti-CSRF Token\n\nThis token is only required when performing browser flows.", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to profile when trying to update a profile.", + "type": "string" + }, + "screen": { + "description": "Screen requests navigation to a previous screen.\n\nThis must be set to credential-selection to go back to the credential\nselection screen.", + "type": "string" + }, + "traits": { + "description": "Traits\n\nThe identity's traits.", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + }, + "required": [ + "traits", + "method" + ], + "type": "object" + }, "updateRegistrationFlowWithWebAuthnMethod": { "description": "Update Registration Flow with WebAuthn Method", "properties": { @@ -4030,6 +4066,8 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], @@ -4269,7 +4307,7 @@ } }, { - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "in": "path", "name": "type", "required": true, @@ -4281,12 +4319,14 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string" }, - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index df1ddfe4a6b4..4621ef4fbfca 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -433,6 +433,8 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], @@ -692,12 +694,14 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "name": "type", "in": "path", "required": true @@ -4109,7 +4113,7 @@ } }, "type": { - "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -4118,10 +4122,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "updated_at": { "description": "UpdatedAt is a helper struct field for gobuffalo.pop.", @@ -4393,7 +4399,7 @@ ], "properties": { "active": { - "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -4402,10 +4408,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "created_at": { "description": "CreatedAt is a helper struct field for gobuffalo.pop.", @@ -4825,7 +4833,7 @@ ], "properties": { "active": { - "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -4834,10 +4842,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "expires_at": { "description": "ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in,\na new flow has to be initiated.", @@ -4989,7 +4999,7 @@ "format": "date-time" }, "method": { - "description": "The method used in this authenticator.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "The method used in this authenticator.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -4998,10 +5008,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "organization": { "description": "The Organization id used for authentication", @@ -6022,6 +6034,36 @@ } } }, + "updateRegistrationFlowWithProfileMethod": { + "description": "Update Registration Flow with Profile Method", + "type": "object", + "required": [ + "traits", + "method" + ], + "properties": { + "csrf_token": { + "description": "The Anti-CSRF Token\n\nThis token is only required when performing browser flows.", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to profile when trying to update a profile.", + "type": "string" + }, + "screen": { + "description": "Screen requests navigation to a previous screen.\n\nThis must be set to credential-selection to go back to the credential\nselection screen.", + "type": "string" + }, + "traits": { + "description": "Traits\n\nThe identity's traits.", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, "updateRegistrationFlowWithWebAuthnMethod": { "description": "Update Registration Flow with WebAuthn Method", "type": "object", From 49e1a390d555f96726fc65f8ffec4c3f607b6866 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:45:47 +0100 Subject: [PATCH 036/262] chore(deps): bump github.com/go-jose/go-jose/v3 from 3.0.1 to 3.0.3 (#3805) Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.1 to 3.0.3. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/v3.0.3/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v3.0.1...v3.0.3) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 04a6ea917423..14593dd5874f 100644 --- a/go.mod +++ b/go.mod @@ -147,7 +147,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-crypt/x v0.2.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect diff --git a/go.sum b/go.sum index 7db827552f74..9db641152989 100644 --- a/go.sum +++ b/go.sum @@ -208,8 +208,8 @@ github.com/go-faker/faker/v4 v4.2.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -406,6 +406,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v27 v27.0.1 h1:sSMFSShNn4VnqCqs+qhab6TS3uQc+uVR6TD1bW6MavM= @@ -1094,7 +1095,6 @@ golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaE golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= From a6ad983ac83aa3ea65c4dc0c46b582096574c25a Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Tue, 12 Mar 2024 15:05:35 +0100 Subject: [PATCH 037/262] feat: linkedin v2 provider (#3804) * feat: add linkedin-v2 provider * docs: document linkedin special-case --- .gitignore | 2 + driver/config/config_test.go | 23 ++------ embedx/config.schema.json | 1 + ...hould_include_OIDC_credentials_config.json | 1 + identity/handler.go | 4 +- identity/handler_test.go | 14 +++-- internal/client-go/go.sum | 1 + selfservice/strategy/oidc/provider.go | 27 ++++++++- selfservice/strategy/oidc/provider_config.go | 41 +++++++------- selfservice/strategy/oidc/provider_discord.go | 2 +- .../strategy/oidc/provider_linkedin_v2.go | 47 ++++++++++++++++ .../oidc/provider_linkedin_v2_test.go | 34 +++++++++++ .../oidc/provider_private_net_test.go | 1 + selfservice/strategy/oidc/provider_test.go | 56 ++++++++++++++++++- .../profiles/passkey/flows.spec.ts | 10 +++- .../two-steps/registration/oidc.spec.ts | 10 +++- 16 files changed, 220 insertions(+), 54 deletions(-) create mode 100644 identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json create mode 100644 selfservice/strategy/oidc/provider_linkedin_v2.go create mode 100644 selfservice/strategy/oidc/provider_linkedin_v2_test.go diff --git a/.gitignore b/.gitignore index d91c2fb7d111..f792761b2b71 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ heap_profiler/ goroutine_dump/ inflight_trace_dump/ +contrib/quickstart/kratos/oidc + e2e/*.log e2e/kratos.*.yml e2e/proxy.json diff --git a/driver/config/config_test.go b/driver/config/config_test.go index b2047e2fc433..2a8d57e7bebb 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -15,7 +15,6 @@ import ( "os" "path/filepath" "strings" - "sync" "testing" "time" @@ -1043,35 +1042,23 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Cleanup(cancel) _, hook, writeSchema := testWatch(t, ctx, &cobra.Command{}, identity) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - // Change the identity config to an invalid file - writeSchema(invalidIdentity.Identity.Schemas) - }() + writeSchema(invalidIdentity.Identity.Schemas) // There are a bunch of log messages beeing logged. We are looking for a specific one. - timeout := time.After(time.Millisecond * 500) - success := false - for !success { + for { for _, v := range hook.AllEntries() { s, err := v.String() require.NoError(t, err) - success = success || strings.Contains(s, "The changed identity schema configuration is invalid and could not be loaded.") + if strings.Contains(s, "The changed identity schema configuration is invalid and could not be loaded.") { + return + } } - select { case <-ctx.Done(): t.Fatal("the test could not complete as the context timed out before the file watcher updated") - case <-timeout: - t.Fatal("Expected log line was not encountered within specified timeout") default: // nothing } } - - wg.Wait() }) } }) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index c7a7be169224..473f0bf514fd 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -436,6 +436,7 @@ "dingtalk", "patreon", "linkedin", + "linkedin_v2", "lark", "x" ], diff --git a/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json b/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json new file mode 100644 index 000000000000..95bff506986a --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json @@ -0,0 +1 @@ +"{\"providers\":[{\"initial_id_token\":\"id_token0\",\"initial_access_token\":\"access_token0\",\"initial_refresh_token\":\"refresh_token0\",\"subject\":\"foo\",\"provider\":\"bar\",\"organization\":\"\"},{\"initial_id_token\":\"id_token1\",\"initial_access_token\":\"access_token1\",\"initial_refresh_token\":\"refresh_token1\",\"subject\":\"baz\",\"provider\":\"zab\",\"organization\":\"\"}]}" diff --git a/identity/handler.go b/identity/handler.go index 0343567a0ac7..8622a2e76d8e 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -251,7 +251,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para } // Identities using the marshaler for including metadata_admin - isam := make([]WithCredentialsMetadataAndAdminMetadataInJSON, len(is)) + isam := make([]WithCredentialsAndAdminMetadataInJSON, len(is)) for i, identity := range is { emit, err := identity.WithDeclassifiedCredentials(r.Context(), h.r, params.DeclassifyCredentials) if err != nil { @@ -259,7 +259,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para return } - isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(*emit) + isam[i] = WithCredentialsAndAdminMetadataInJSON(*emit) } h.r.Writer().Write(w, r, isam) diff --git a/identity/handler_test.go b/identity/handler_test.go index 9444da92a0e8..bfdf1a86dfc3 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -1348,11 +1348,15 @@ func TestHandler(t *testing.T) { }) t.Run("case=should list all identities with credentials", func(t *testing.T) { - res := get(t, adminTS, "/identities?include_credential=totp", http.StatusOK) - assert.True(t, res.Get("0.credentials").Exists(), "credentials config should be included: %s", res.Raw) - assert.True(t, res.Get("0.metadata_public").Exists(), "metadata_public config should be included: %s", res.Raw) - assert.True(t, res.Get("0.metadata_admin").Exists(), "metadata_admin config should be included: %s", res.Raw) - assert.EqualValues(t, "baz", res.Get(`#(traits.bar=="baz").traits.bar`).String(), "%s", res.Raw) + t.Run("include_credential=oidc should include OIDC credentials config", func(t *testing.T) { + res := get(t, adminTS, "/identities?include_credential=oidc&credentials_identifier=bar:foo.oidc@bar.com", http.StatusOK) + assert.True(t, res.Get("0.credentials.oidc.config").Exists(), "credentials config should be included: %s", res.Raw) + snapshotx.SnapshotT(t, res.Get("0.credentials.oidc.config").String()) + }) + t.Run("include_credential=totp should not include OIDC credentials config", func(t *testing.T) { + res := get(t, adminTS, "/identities?include_credential=totp&credentials_identifier=bar:foo.oidc@bar.com", http.StatusOK) + assert.False(t, res.Get("0.credentials.oidc.config").Exists(), "credentials config should be included: %s", res.Raw) + }) }) t.Run("case=should not be able to list all identities with credentials due to wrong credentials type", func(t *testing.T) { diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go index cb22ebb6b847..30ea305a22ed 100644 --- a/selfservice/strategy/oidc/provider.go +++ b/selfservice/strategy/oidc/provider.go @@ -5,8 +5,10 @@ package oidc import ( "context" + "encoding/json" "net/http" "net/url" + "strings" "github.com/dghubble/oauth1" "github.com/pkg/errors" @@ -68,7 +70,7 @@ type Claims struct { Gender string `json:"gender,omitempty"` Birthdate string `json:"birthdate,omitempty"` Zoneinfo string `json:"zoneinfo,omitempty"` - Locale string `json:"locale,omitempty"` + Locale Locale `json:"locale,omitempty"` PhoneNumber string `json:"phone_number,omitempty"` PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` UpdatedAt int64 `json:"updated_at,omitempty"` @@ -79,6 +81,29 @@ type Claims struct { RawClaims map[string]interface{} `json:"raw_claims,omitempty"` } +type Locale string + +func (l *Locale) UnmarshalJSON(data []byte) error { + var linkedInLocale struct { + Language string `json:"language"` + Country string `json:"country"` + } + if err := json.Unmarshal(data, &linkedInLocale); err == nil { + switch { + case linkedInLocale.Language == "": + *l = Locale(linkedInLocale.Country) + case linkedInLocale.Country == "": + *l = Locale(linkedInLocale.Language) + default: + *l = Locale(strings.Join([]string{linkedInLocale.Language, linkedInLocale.Country}, "-")) + } + + return nil + } + + return json.Unmarshal(data, (*string)(l)) +} + // Validate checks if the claims are valid. func (c *Claims) Validate() error { if c.Subject == "" { diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index 0e579906a94f..4eac8be4f7db 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -141,26 +141,27 @@ type ConfigurationCollection struct { // If you add a provider here, please also add a test to // provider_private_net_test.go var supportedProviders = map[string]func(config *Configuration, reg Dependencies) Provider{ - "generic": NewProviderGenericOIDC, - "google": NewProviderGoogle, - "github": NewProviderGitHub, - "github-app": NewProviderGitHubApp, - "gitlab": NewProviderGitLab, - "microsoft": NewProviderMicrosoft, - "discord": NewProviderDiscord, - "slack": NewProviderSlack, - "facebook": NewProviderFacebook, - "auth0": NewProviderAuth0, - "vk": NewProviderVK, - "yandex": NewProviderYandex, - "apple": NewProviderApple, - "spotify": NewProviderSpotify, - "netid": NewProviderNetID, - "dingtalk": NewProviderDingTalk, - "linkedin": NewProviderLinkedIn, - "patreon": NewProviderPatreon, - "lark": NewProviderLark, - "x": NewProviderX, + "generic": NewProviderGenericOIDC, + "google": NewProviderGoogle, + "github": NewProviderGitHub, + "github-app": NewProviderGitHubApp, + "gitlab": NewProviderGitLab, + "microsoft": NewProviderMicrosoft, + "discord": NewProviderDiscord, + "slack": NewProviderSlack, + "facebook": NewProviderFacebook, + "auth0": NewProviderAuth0, + "vk": NewProviderVK, + "yandex": NewProviderYandex, + "apple": NewProviderApple, + "spotify": NewProviderSpotify, + "netid": NewProviderNetID, + "dingtalk": NewProviderDingTalk, + "linkedin": NewProviderLinkedIn, + "linkedin_v2": NewProviderLinkedInV2, + "patreon": NewProviderPatreon, + "lark": NewProviderLark, + "x": NewProviderX, } func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) { diff --git a/selfservice/strategy/oidc/provider_discord.go b/selfservice/strategy/oidc/provider_discord.go index 181e7df6d322..99bea24d5770 100644 --- a/selfservice/strategy/oidc/provider_discord.go +++ b/selfservice/strategy/oidc/provider_discord.go @@ -93,7 +93,7 @@ func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, qu Picture: user.AvatarURL(""), Email: user.Email, EmailVerified: x.ConvertibleBoolean(user.Verified), - Locale: user.Locale, + Locale: Locale(user.Locale), } return claims, nil diff --git a/selfservice/strategy/oidc/provider_linkedin_v2.go b/selfservice/strategy/oidc/provider_linkedin_v2.go new file mode 100644 index 000000000000..7ce40239ef46 --- /dev/null +++ b/selfservice/strategy/oidc/provider_linkedin_v2.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "net/url" + + gooidc "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +type ProviderLinkedInV2 struct { + *ProviderGenericOIDC +} + +func NewProviderLinkedInV2( + config *Configuration, + reg Dependencies, +) Provider { + config.ClaimsSource = ClaimsSourceUserInfo + config.IssuerURL = "https://www.linkedin.com/oauth" + + return &ProviderLinkedInV2{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (l *ProviderLinkedInV2) wrapCtx(ctx context.Context) context.Context { + // We need to overwrite the issuer here because the discovery URL is under + // `https://www.linkedin.com/oauth/.well-known/openid-configuration`, wherease + // the issuer is `https://www.linkedin.com` (without the `/oauth`). This is + // not conformant according to the OIDC spec, but needed for LinkedIn. + return gooidc.InsecureIssuerURLContext(ctx, "https://www.linkedin.com") +} + +func (l *ProviderLinkedInV2) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return l.ProviderGenericOIDC.OAuth2(l.wrapCtx(ctx)) +} + +func (l *ProviderLinkedInV2) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + return l.ProviderGenericOIDC.Claims(l.wrapCtx(ctx), exchange, query) +} diff --git a/selfservice/strategy/oidc/provider_linkedin_v2_test.go b/selfservice/strategy/oidc/provider_linkedin_v2_test.go new file mode 100644 index 000000000000..c36e44473fa7 --- /dev/null +++ b/selfservice/strategy/oidc/provider_linkedin_v2_test.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestProviderLinkedInV2_Discovery(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + + p := oidc.NewProviderLinkedInV2(&oidc.Configuration{ + Provider: "linkedin_v2", + ID: "valid", + ClientID: "client", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{"email", "profile", "offline_access"}, + }, reg) + + c, err := p.(oidc.OAuth2Provider).OAuth2(context.Background()) + require.NoError(t, err) + assert.Contains(t, c.Scopes, "openid") + assert.Equal(t, "https://www.linkedin.com/oauth/v2/accessToken", c.Endpoint.TokenURL) +} diff --git a/selfservice/strategy/oidc/provider_private_net_test.go b/selfservice/strategy/oidc/provider_private_net_test.go index e656ee0462bb..0505a3e19626 100644 --- a/selfservice/strategy/oidc/provider_private_net_test.go +++ b/selfservice/strategy/oidc/provider_private_net_test.go @@ -73,6 +73,7 @@ func TestProviderPrivateIP(t *testing.T) { // GitHub uses a fixed token URL and does not use the issuer. // GitHub App uses a fixed token URL and does not use the issuer. // GitHub App uses a fixed token URL and does not use the issuer. + // LinkedInV2 uses a fixed token URL and does not use the issuer. {p: gitlab, c: &oidc.Configuration{IssuerURL: "http://127.0.0.2/"}, e: "is not a permitted destination"}, // The TokenURL is fixed in GitLab to {issuer_url}/token. Since the issuer is called first, any local token fails also. diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go index 7c0de7c55138..a5733d2e95f8 100644 --- a/selfservice/strategy/oidc/provider_test.go +++ b/selfservice/strategy/oidc/provider_test.go @@ -9,6 +9,7 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -42,7 +43,7 @@ func RegisterTestProvider(id string) func() { var _ IDTokenVerifier = new(TestProvider) -func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error) { +func (t *TestProvider) Verify(_ context.Context, token string) (*Claims, error) { if token == "error" { return nil, fmt.Errorf("stub error") } @@ -52,3 +53,56 @@ func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error } return &c, nil } + +func TestLocale(t *testing.T) { + // test json unmarshal + for _, tc := range []struct { + name string + json string + expected string + assertErr assert.ErrorAssertionFunc + }{{ + name: "empty", + json: `{}`, + expected: "", + }, { + name: "empty string locale", + json: `{"locale":""}`, + expected: "", + }, { + name: "invalid string locale", + json: `{"locale":"""}`, + assertErr: assert.Error, + }, { + name: "string locale", + json: `{"locale":"en-US"}`, + expected: "en-US", + }, { + name: "linkedin locale", + json: `{"locale":{"country":"US","language":"en","ignore":"me"}}`, + expected: "en-US", + }, { + name: "missing country linkedin locale", + json: `{"locale":{"language":"en"}}`, + expected: "en", + }, { + name: "missing language linkedin locale", + json: `{"locale":{"country":"US"}}`, + expected: "US", + }, { + name: "invalid linkedin locale", + json: `{"locale":{"invalid":"me"}}`, + expected: "", + }} { + t.Run(tc.name, func(t *testing.T) { + var c Claims + err := json.Unmarshal([]byte(tc.json), &c) + if tc.assertErr != nil { + tc.assertErr(t, err) + return + } + require.NoError(t, err) + assert.EqualValues(t, tc.expected, c.Locale) + }) + } +} diff --git a/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts index 95fafafa0f30..78426b7cd9f4 100644 --- a/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts +++ b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts @@ -4,7 +4,7 @@ import { gen } from "../../../helpers" import { routes as express } from "../../../helpers/express" import { routes as react } from "../../../helpers/react" -import { testRegistrationWebhook } from "../../../helpers/webhook" +import { testFlowWebhook } from "../../../helpers/webhook" const signup = (registration: string, app: string, email = gen.email()) => { cy.visit(registration) @@ -158,8 +158,12 @@ context("Passkey registration", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "passkey", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "passkey", [ + ...hooks, + { hook: "session" }, + ]), () => { signup(registration, app) }, diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts index 33b8bafe8735..cd4cf069c556 100644 --- a/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts @@ -4,7 +4,7 @@ import { appPrefix, gen, website } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" -import { testRegistrationWebhook } from "../../../../helpers/webhook" +import { testFlowWebhook } from "../../../../helpers/webhook" context("Social Sign Up Successes", () => { ;[ @@ -104,8 +104,12 @@ context("Social Sign Up Successes", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "oidc", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "oidc", [ + ...hooks, + { hook: "session" }, + ]), () => { const email = gen.email() cy.registerOidc({ From 14594039a0fc9492ea4d821419c337608c23a22b Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:07:12 +0000 Subject: [PATCH 038/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From bdf992e5ad762c5fad0377b8e9fd0ab80c39744f Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:53:29 +0000 Subject: [PATCH 039/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 65 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4160b8af7fe..7cba2ae2b63c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,14 @@ **Table of Contents** -- [ (2024-03-08)](#2024-03-08) - - [Bug Fixes](#bug-fixes) - - [Features](#features) - - [Tests](#tests) - - [Unclassified](#unclassified) -- [1.1.0 (2024-02-20)](#110-2024-02-20) +- [ (2024-03-12)](#2024-03-12) - [Breaking Changes](#breaking-changes) + - [Bug Fixes](#bug-fixes) + - [Features](#features) + - [Tests](#tests) + - [Unclassified](#unclassified) +- [1.1.0 (2024-02-20)](#110-2024-02-20) + - [Breaking Changes](#breaking-changes-1) - [Bug Fixes](#bug-fixes-1) - [Code Generation](#code-generation) - [Documentation](#documentation) @@ -27,7 +28,7 @@ - [Tests](#tests-2) - [Unclassified](#unclassified-2) - [0.13.0 (2023-04-18)](#0130-2023-04-18) - - [Breaking Changes](#breaking-changes-1) + - [Breaking Changes](#breaking-changes-2) - [Bug Fixes](#bug-fixes-3) - [Code Generation](#code-generation-2) - [Code Refactoring](#code-refactoring) @@ -36,7 +37,7 @@ - [Tests](#tests-3) - [Unclassified](#unclassified-3) - [0.11.1 (2023-01-14)](#0111-2023-01-14) - - [Breaking Changes](#breaking-changes-2) + - [Breaking Changes](#breaking-changes-3) - [Bug Fixes](#bug-fixes-4) - [Code Generation](#code-generation-3) - [Documentation](#documentation-3) @@ -46,7 +47,7 @@ - [Code Generation](#code-generation-4) - [Features](#features-5) - [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28) - - [Breaking Changes](#breaking-changes-3) + - [Breaking Changes](#breaking-changes-4) - [Bug Fixes](#bug-fixes-5) - [Code Generation](#code-generation-5) - [Code Refactoring](#code-refactoring-1) @@ -59,7 +60,7 @@ - [Bug Fixes](#bug-fixes-6) - [Code Generation](#code-generation-6) - [0.10.0 (2022-05-30)](#0100-2022-05-30) - - [Breaking Changes](#breaking-changes-4) + - [Breaking Changes](#breaking-changes-5) - [Bug Fixes](#bug-fixes-7) - [Code Generation](#code-generation-7) - [Code Refactoring](#code-refactoring-2) @@ -68,7 +69,7 @@ - [Tests](#tests-6) - [Unclassified](#unclassified-5) - [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25) - - [Breaking Changes](#breaking-changes-5) + - [Breaking Changes](#breaking-changes-6) - [Bug Fixes](#bug-fixes-8) - [Code Generation](#code-generation-8) - [Documentation](#documentation-6) @@ -76,7 +77,7 @@ - [Bug Fixes](#bug-fixes-9) - [Code Generation](#code-generation-9) - [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21) - - [Breaking Changes](#breaking-changes-6) + - [Breaking Changes](#breaking-changes-7) - [Bug Fixes](#bug-fixes-10) - [Code Generation](#code-generation-10) - [Code Refactoring](#code-refactoring-3) @@ -85,7 +86,7 @@ - [Tests](#tests-7) - [Unclassified](#unclassified-6) - [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21) - - [Breaking Changes](#breaking-changes-7) + - [Breaking Changes](#breaking-changes-8) - [Bug Fixes](#bug-fixes-11) - [Code Generation](#code-generation-11) - [Code Refactoring](#code-refactoring-4) @@ -103,7 +104,7 @@ - [Features](#features-10) - [Tests](#tests-9) - [0.8.0-alpha.4.pre.0 (2021-11-09)](#080-alpha4pre0-2021-11-09) - - [Breaking Changes](#breaking-changes-8) + - [Breaking Changes](#breaking-changes-9) - [Bug Fixes](#bug-fixes-14) - [Code Generation](#code-generation-14) - [Documentation](#documentation-11) @@ -115,7 +116,7 @@ - [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28) - [Code Generation](#code-generation-16) - [0.8.0-alpha.1 (2021-10-27)](#080-alpha1-2021-10-27) - - [Breaking Changes](#breaking-changes-9) + - [Breaking Changes](#breaking-changes-10) - [Bug Fixes](#bug-fixes-16) - [Code Generation](#code-generation-17) - [Code Refactoring](#code-refactoring-5) @@ -145,7 +146,7 @@ - [Documentation](#documentation-15) - [Tests](#tests-13) - [0.7.0-alpha.1 (2021-07-13)](#070-alpha1-2021-07-13) - - [Breaking Changes](#breaking-changes-10) + - [Breaking Changes](#breaking-changes-11) - [Bug Fixes](#bug-fixes-20) - [Code Generation](#code-generation-23) - [Code Refactoring](#code-refactoring-6) @@ -154,7 +155,7 @@ - [Tests](#tests-14) - [Unclassified](#unclassified-8) - [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17) - - [Breaking Changes](#breaking-changes-11) + - [Breaking Changes](#breaking-changes-12) - [Bug Fixes](#bug-fixes-21) - [Code Generation](#code-generation-24) - [Code Refactoring](#code-refactoring-7) @@ -169,7 +170,7 @@ - [Code Generation](#code-generation-27) - [Features](#features-17) - [0.6.0-alpha.1 (2021-05-05)](#060-alpha1-2021-05-05) - - [Breaking Changes](#breaking-changes-12) + - [Breaking Changes](#breaking-changes-13) - [Bug Fixes](#bug-fixes-23) - [Code Generation](#code-generation-28) - [Code Refactoring](#code-refactoring-8) @@ -209,7 +210,7 @@ - [Tests](#tests-19) - [Unclassified](#unclassified-11) - [0.5.0-alpha.1 (2020-10-15)](#050-alpha1-2020-10-15) - - [Breaking Changes](#breaking-changes-13) + - [Breaking Changes](#breaking-changes-14) - [Bug Fixes](#bug-fixes-29) - [Code Generation](#code-generation-34) - [Code Refactoring](#code-refactoring-10) @@ -234,7 +235,7 @@ - [Bug Fixes](#bug-fixes-34) - [Code Generation](#code-generation-39) - [0.4.0-alpha.1 (2020-07-08)](#040-alpha1-2020-07-08) - - [Breaking Changes](#breaking-changes-14) + - [Breaking Changes](#breaking-changes-15) - [Bug Fixes](#bug-fixes-35) - [Code Generation](#code-generation-40) - [Code Refactoring](#code-refactoring-11) @@ -242,7 +243,7 @@ - [Features](#features-24) - [Unclassified](#unclassified-13) - [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15) - - [Breaking Changes](#breaking-changes-15) + - [Breaking Changes](#breaking-changes-16) - [Bug Fixes](#bug-fixes-36) - [Chores](#chores) - [Code Refactoring](#code-refactoring-12) @@ -253,7 +254,7 @@ - [Chores](#chores-1) - [Documentation](#documentation-28) - [0.2.0-alpha.2 (2020-05-04)](#020-alpha2-2020-05-04) - - [Breaking Changes](#breaking-changes-16) + - [Breaking Changes](#breaking-changes-17) - [Bug Fixes](#bug-fixes-37) - [Chores](#chores-2) - [Code Refactoring](#code-refactoring-13) @@ -321,7 +322,15 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-08) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-12) + +## Breaking Changes + +This feature enables two-step registration per default. Two-step registration is +a significantly improved sign up flow and recommended when using more than one +sign up methods. To disable two-step registration, set +`selfservice.flows.registration.enable_legacy_flow` to `true`. This value +defaults to `false`. ### Bug Fixes @@ -369,6 +378,16 @@ ([930fb19](https://github.com/ory/kratos/commit/930fb19842e527e5e9c415efa983b36e02829516)) - Control edge cache ttl ([#3808](https://github.com/ory/kratos/issues/3808)) ([c9dcce5](https://github.com/ory/kratos/commit/c9dcce5a41137937df1aad7ac81170b443740f88)) +- Linkedin v2 provider ([#3804](https://github.com/ory/kratos/issues/3804)) + ([a6ad983](https://github.com/ory/kratos/commit/a6ad983ac83aa3ea65c4dc0c46b582096574c25a)): + + - feat: add linkedin-v2 provider + + - docs: document linkedin special-case + +- PassKeys with Resident Keys and two-step registration + ([#3748](https://github.com/ory/kratos/issues/3748)) + ([3621411](https://github.com/ory/kratos/commit/3621411dc4386d841bc6766a5ab8d03e65812073)) - Send OIDC claim keys to tracing ([#3798](https://github.com/ory/kratos/issues/3798)) ([04390be](https://github.com/ory/kratos/commit/04390bee426befe51af2ee8177afabaa9ce4fa80)) From 9ddf7cc7c52313c4ee13ccdc2886ad94b5d1317f Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:36:44 +0100 Subject: [PATCH 040/262] fix(sdk): improve discriminators for node and Go (#3821) --- .schema/openapi/gen.go.yml | 1 + internal/client-go/model_continue_with.go | 132 +++++++----- .../model_ui_node_anchor_attributes.go | 2 +- .../client-go/model_ui_node_attributes.go | 162 +++++++++------ .../model_ui_node_image_attributes.go | 2 +- .../model_ui_node_input_attributes.go | 2 +- .../model_ui_node_script_attributes.go | 2 +- .../model_ui_node_text_attributes.go | 2 +- .../client-go/model_update_login_flow_body.go | 192 ++++++++++++------ .../model_update_recovery_flow_body.go | 74 ++++--- .../model_update_registration_flow_body.go | 132 +++++++----- .../model_update_settings_flow_body.go | 192 ++++++++++++------ .../model_update_verification_flow_body.go | 74 ++++--- internal/httpclient/model_continue_with.go | 132 +++++++----- .../model_ui_node_anchor_attributes.go | 2 +- .../httpclient/model_ui_node_attributes.go | 162 +++++++++------ .../model_ui_node_image_attributes.go | 2 +- .../model_ui_node_input_attributes.go | 2 +- .../model_ui_node_script_attributes.go | 2 +- .../model_ui_node_text_attributes.go | 2 +- .../model_update_login_flow_body.go | 192 ++++++++++++------ .../model_update_recovery_flow_body.go | 74 ++++--- .../model_update_registration_flow_body.go | 132 +++++++----- .../model_update_settings_flow_body.go | 192 ++++++++++++------ .../model_update_verification_flow_body.go | 74 ++++--- spec/api.json | 60 +++++- spec/swagger.json | 60 +++++- ui/node/attributes.go | 10 +- ui/node/node.go | 20 +- 29 files changed, 1360 insertions(+), 727 deletions(-) diff --git a/.schema/openapi/gen.go.yml b/.schema/openapi/gen.go.yml index 48cee4db3b58..0ca7bcda46b9 100644 --- a/.schema/openapi/gen.go.yml +++ b/.schema/openapi/gen.go.yml @@ -4,3 +4,4 @@ generateInterfaces: true isGoSubmodule: false structPrefix: true enumClassPrefix: true +useOneOfDiscriminatorLookup: true diff --git a/internal/client-go/model_continue_with.go b/internal/client-go/model_continue_with.go index ee84e74692fb..9e97dbf479e7 100644 --- a/internal/client-go/model_continue_with.go +++ b/internal/client-go/model_continue_with.go @@ -55,72 +55,110 @@ func ContinueWithVerificationUiAsContinueWith(v *ContinueWithVerificationUi) Con // Unmarshal JSON data into one of the pointers in the struct func (dst *ContinueWith) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into ContinueWithRecoveryUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithRecoveryUi) - if err == nil { - jsonContinueWithRecoveryUi, _ := json.Marshal(dst.ContinueWithRecoveryUi) - if string(jsonContinueWithRecoveryUi) == "{}" { // empty struct - dst.ContinueWithRecoveryUi = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'set_ory_session_token' + if jsonDict["action"] == "set_ory_session_token" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match } else { - match++ + dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) } - } else { - dst.ContinueWithRecoveryUi = nil } - // try to unmarshal data into ContinueWithSetOrySessionToken - err = newStrictDecoder(data).Decode(&dst.ContinueWithSetOrySessionToken) - if err == nil { - jsonContinueWithSetOrySessionToken, _ := json.Marshal(dst.ContinueWithSetOrySessionToken) - if string(jsonContinueWithSetOrySessionToken) == "{}" { // empty struct - dst.ContinueWithSetOrySessionToken = nil + // check if the discriminator value is 'show_recovery_ui' + if jsonDict["action"] == "show_recovery_ui" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithSetOrySessionToken = nil } - // try to unmarshal data into ContinueWithSettingsUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithSettingsUi) - if err == nil { - jsonContinueWithSettingsUi, _ := json.Marshal(dst.ContinueWithSettingsUi) - if string(jsonContinueWithSettingsUi) == "{}" { // empty struct - dst.ContinueWithSettingsUi = nil + // check if the discriminator value is 'show_settings_ui' + if jsonDict["action"] == "show_settings_ui" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match } else { - match++ + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) } - } else { - dst.ContinueWithSettingsUi = nil } - // try to unmarshal data into ContinueWithVerificationUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithVerificationUi) - if err == nil { - jsonContinueWithVerificationUi, _ := json.Marshal(dst.ContinueWithVerificationUi) - if string(jsonContinueWithVerificationUi) == "{}" { // empty struct + // check if the discriminator value is 'show_verification_ui' + if jsonDict["action"] == "show_verification_ui" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithRecoveryUi' + if jsonDict["action"] == "continueWithRecoveryUi" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithVerificationUi = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.ContinueWithRecoveryUi = nil - dst.ContinueWithSetOrySessionToken = nil - dst.ContinueWithSettingsUi = nil - dst.ContinueWithVerificationUi = nil + // check if the discriminator value is 'continueWithSetOrySessionToken' + if jsonDict["action"] == "continueWithSetOrySessionToken" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match + } else { + dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithSettingsUi' + if jsonDict["action"] == "continueWithSettingsUi" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match + } else { + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(ContinueWith)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(ContinueWith)") + // check if the discriminator value is 'continueWithVerificationUi' + if jsonDict["action"] == "continueWithVerificationUi" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { + dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_ui_node_anchor_attributes.go b/internal/client-go/model_ui_node_anchor_attributes.go index 15cb492fe8eb..ad2cc992a119 100644 --- a/internal/client-go/model_ui_node_anchor_attributes.go +++ b/internal/client-go/model_ui_node_anchor_attributes.go @@ -21,7 +21,7 @@ type UiNodeAnchorAttributes struct { Href string `json:"href"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Title UiText `json:"title"` } diff --git a/internal/client-go/model_ui_node_attributes.go b/internal/client-go/model_ui_node_attributes.go index 347be6f44a72..510dc20f8564 100644 --- a/internal/client-go/model_ui_node_attributes.go +++ b/internal/client-go/model_ui_node_attributes.go @@ -63,86 +63,134 @@ func UiNodeTextAttributesAsUiNodeAttributes(v *UiNodeTextAttributes) UiNodeAttri // Unmarshal JSON data into one of the pointers in the struct func (dst *UiNodeAttributes) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UiNodeAnchorAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeAnchorAttributes) - if err == nil { - jsonUiNodeAnchorAttributes, _ := json.Marshal(dst.UiNodeAnchorAttributes) - if string(jsonUiNodeAnchorAttributes) == "{}" { // empty struct - dst.UiNodeAnchorAttributes = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'a' + if jsonDict["node_type"] == "a" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match } else { - match++ + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) } - } else { - dst.UiNodeAnchorAttributes = nil } - // try to unmarshal data into UiNodeImageAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeImageAttributes) - if err == nil { - jsonUiNodeImageAttributes, _ := json.Marshal(dst.UiNodeImageAttributes) - if string(jsonUiNodeImageAttributes) == "{}" { // empty struct - dst.UiNodeImageAttributes = nil + // check if the discriminator value is 'img' + if jsonDict["node_type"] == "img" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeImageAttributes = nil } - // try to unmarshal data into UiNodeInputAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeInputAttributes) - if err == nil { - jsonUiNodeInputAttributes, _ := json.Marshal(dst.UiNodeInputAttributes) - if string(jsonUiNodeInputAttributes) == "{}" { // empty struct - dst.UiNodeInputAttributes = nil + // check if the discriminator value is 'input' + if jsonDict["node_type"] == "input" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match } else { - match++ + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) } - } else { - dst.UiNodeInputAttributes = nil } - // try to unmarshal data into UiNodeScriptAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeScriptAttributes) - if err == nil { - jsonUiNodeScriptAttributes, _ := json.Marshal(dst.UiNodeScriptAttributes) - if string(jsonUiNodeScriptAttributes) == "{}" { // empty struct - dst.UiNodeScriptAttributes = nil + // check if the discriminator value is 'script' + if jsonDict["node_type"] == "script" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match } else { - match++ + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) } - } else { - dst.UiNodeScriptAttributes = nil } - // try to unmarshal data into UiNodeTextAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeTextAttributes) - if err == nil { - jsonUiNodeTextAttributes, _ := json.Marshal(dst.UiNodeTextAttributes) - if string(jsonUiNodeTextAttributes) == "{}" { // empty struct + // check if the discriminator value is 'text' + if jsonDict["node_type"] == "text" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeAnchorAttributes' + if jsonDict["node_type"] == "uiNodeAnchorAttributes" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match + } else { + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeImageAttributes' + if jsonDict["node_type"] == "uiNodeImageAttributes" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeTextAttributes = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UiNodeAnchorAttributes = nil - dst.UiNodeImageAttributes = nil - dst.UiNodeInputAttributes = nil - dst.UiNodeScriptAttributes = nil - dst.UiNodeTextAttributes = nil + // check if the discriminator value is 'uiNodeInputAttributes' + if jsonDict["node_type"] == "uiNodeInputAttributes" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match + } else { + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UiNodeAttributes)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UiNodeAttributes)") + // check if the discriminator value is 'uiNodeScriptAttributes' + if jsonDict["node_type"] == "uiNodeScriptAttributes" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match + } else { + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeTextAttributes' + if jsonDict["node_type"] == "uiNodeTextAttributes" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { + dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_ui_node_image_attributes.go b/internal/client-go/model_ui_node_image_attributes.go index 5b300b9548bd..6eef160e3d67 100644 --- a/internal/client-go/model_ui_node_image_attributes.go +++ b/internal/client-go/model_ui_node_image_attributes.go @@ -21,7 +21,7 @@ type UiNodeImageAttributes struct { Height int64 `json:"height"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // The image's source URL. format: uri Src string `json:"src"` diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go index fbf7e0f1b04e..b373dda7ccfd 100644 --- a/internal/client-go/model_ui_node_input_attributes.go +++ b/internal/client-go/model_ui_node_input_attributes.go @@ -24,7 +24,7 @@ type UiNodeInputAttributes struct { Label *UiText `json:"label,omitempty"` // The input's element name. Name string `json:"name"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Onclick *string `json:"onclick,omitempty"` diff --git a/internal/client-go/model_ui_node_script_attributes.go b/internal/client-go/model_ui_node_script_attributes.go index 21dca70cbe86..d867c1c66dba 100644 --- a/internal/client-go/model_ui_node_script_attributes.go +++ b/internal/client-go/model_ui_node_script_attributes.go @@ -25,7 +25,7 @@ type UiNodeScriptAttributes struct { Id string `json:"id"` // The script's integrity hash Integrity string `json:"integrity"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // Nonce for CSP A nonce you may want to use to improve your Content Security Policy. You do not have to use this value but if you want to improve your CSP policies you may use it. You can also choose to use your own nonce value! Nonce string `json:"nonce"` diff --git a/internal/client-go/model_ui_node_text_attributes.go b/internal/client-go/model_ui_node_text_attributes.go index 6199187b5d75..93e0f8314191 100644 --- a/internal/client-go/model_ui_node_text_attributes.go +++ b/internal/client-go/model_ui_node_text_attributes.go @@ -19,7 +19,7 @@ import ( type UiNodeTextAttributes struct { // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Text UiText `json:"text"` } diff --git a/internal/client-go/model_update_login_flow_body.go b/internal/client-go/model_update_login_flow_body.go index 36033328e78d..ac3e4f503292 100644 --- a/internal/client-go/model_update_login_flow_body.go +++ b/internal/client-go/model_update_login_flow_body.go @@ -71,100 +71,158 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateLoginFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) - if err == nil { - jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) - if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) - if err == nil { - jsonUpdateLoginFlowWithLookupSecretMethod, _ := json.Marshal(dst.UpdateLoginFlowWithLookupSecretMethod) - if string(jsonUpdateLoginFlowWithLookupSecretMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithLookupSecretMethod = nil + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithLookupSecretMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithOidcMethod) - if err == nil { - jsonUpdateLoginFlowWithOidcMethod, _ := json.Marshal(dst.UpdateLoginFlowWithOidcMethod) - if string(jsonUpdateLoginFlowWithOidcMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithOidcMethod = nil + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithPasswordMethod) - if err == nil { - jsonUpdateLoginFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateLoginFlowWithPasswordMethod) - if string(jsonUpdateLoginFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithTotpMethod) - if err == nil { - jsonUpdateLoginFlowWithTotpMethod, _ := json.Marshal(dst.UpdateLoginFlowWithTotpMethod) - if string(jsonUpdateLoginFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateLoginFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateLoginFlowWithWebAuthnMethod) - if string(jsonUpdateLoginFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithCodeMethod' + if jsonDict["method"] == "updateLoginFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateLoginFlowWithCodeMethod = nil - dst.UpdateLoginFlowWithLookupSecretMethod = nil - dst.UpdateLoginFlowWithOidcMethod = nil - dst.UpdateLoginFlowWithPasswordMethod = nil - dst.UpdateLoginFlowWithTotpMethod = nil - dst.UpdateLoginFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod' + if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match + } else { + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateLoginFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateLoginFlowBody)") + // check if the discriminator value is 'updateLoginFlowWithOidcMethod' + if jsonDict["method"] == "updateLoginFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match + } else { + dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithPasswordMethod' + if jsonDict["method"] == "updateLoginFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithTotpMethod' + if jsonDict["method"] == "updateLoginFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match + } else { + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateLoginFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_update_recovery_flow_body.go b/internal/client-go/model_update_recovery_flow_body.go index 6eea1d5d6b6a..b0f6de861b4f 100644 --- a/internal/client-go/model_update_recovery_flow_body.go +++ b/internal/client-go/model_update_recovery_flow_body.go @@ -39,44 +39,62 @@ func UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryF // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRecoveryFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRecoveryFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithCodeMethod) - if err == nil { - jsonUpdateRecoveryFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithCodeMethod) - if string(jsonUpdateRecoveryFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRecoveryFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithLinkMethod) - if err == nil { - jsonUpdateRecoveryFlowWithLinkMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithLinkMethod) - if string(jsonUpdateRecoveryFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRecoveryFlowWithCodeMethod = nil - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'updateRecoveryFlowWithCodeMethod' + if jsonDict["method"] == "updateRecoveryFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRecoveryFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRecoveryFlowBody)") + // check if the discriminator value is 'updateRecoveryFlowWithLinkMethod' + if jsonDict["method"] == "updateRecoveryFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go index 0e36a95f635f..7272ea1ace1a 100644 --- a/internal/client-go/model_update_registration_flow_body.go +++ b/internal/client-go/model_update_registration_flow_body.go @@ -55,72 +55,110 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) - if err == nil { - jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) - if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) - if err == nil { - jsonUpdateRegistrationFlowWithOidcMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithOidcMethod) - if string(jsonUpdateRegistrationFlowWithOidcMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithOidcMethod = nil + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithPasswordMethod) - if err == nil { - jsonUpdateRegistrationFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithPasswordMethod) - if string(jsonUpdateRegistrationFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateRegistrationFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithWebAuthnMethod) - if string(jsonUpdateRegistrationFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithCodeMethod' + if jsonDict["method"] == "updateRegistrationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRegistrationFlowWithCodeMethod = nil - dst.UpdateRegistrationFlowWithOidcMethod = nil - dst.UpdateRegistrationFlowWithPasswordMethod = nil - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateRegistrationFlowWithOidcMethod' + if jsonDict["method"] == "updateRegistrationFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithPasswordMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRegistrationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRegistrationFlowBody)") + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_update_settings_flow_body.go b/internal/client-go/model_update_settings_flow_body.go index 064ff9b771dd..e2aec380a586 100644 --- a/internal/client-go/model_update_settings_flow_body.go +++ b/internal/client-go/model_update_settings_flow_body.go @@ -71,100 +71,158 @@ func UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(v *UpdateSetti // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateSettingsFlowWithLookupMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithLookupMethod) - if err == nil { - jsonUpdateSettingsFlowWithLookupMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithLookupMethod) - if string(jsonUpdateSettingsFlowWithLookupMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithLookupMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithLookupMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithOidcMethod) - if err == nil { - jsonUpdateSettingsFlowWithOidcMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithOidcMethod) - if string(jsonUpdateSettingsFlowWithOidcMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithOidcMethod = nil + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithPasswordMethod) - if err == nil { - jsonUpdateSettingsFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithPasswordMethod) - if string(jsonUpdateSettingsFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithProfileMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithProfileMethod) - if err == nil { - jsonUpdateSettingsFlowWithProfileMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithProfileMethod) - if string(jsonUpdateSettingsFlowWithProfileMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithProfileMethod = nil + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithProfileMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithTotpMethod) - if err == nil { - jsonUpdateSettingsFlowWithTotpMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithTotpMethod) - if string(jsonUpdateSettingsFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateSettingsFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithWebAuthnMethod) - if string(jsonUpdateSettingsFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithLookupMethod' + if jsonDict["method"] == "updateSettingsFlowWithLookupMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateSettingsFlowWithLookupMethod = nil - dst.UpdateSettingsFlowWithOidcMethod = nil - dst.UpdateSettingsFlowWithPasswordMethod = nil - dst.UpdateSettingsFlowWithProfileMethod = nil - dst.UpdateSettingsFlowWithTotpMethod = nil - dst.UpdateSettingsFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateSettingsFlowWithOidcMethod' + if jsonDict["method"] == "updateSettingsFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateSettingsFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateSettingsFlowBody)") + // check if the discriminator value is 'updateSettingsFlowWithPasswordMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithProfileMethod' + if jsonDict["method"] == "updateSettingsFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithTotpMethod' + if jsonDict["method"] == "updateSettingsFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateSettingsFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_update_verification_flow_body.go b/internal/client-go/model_update_verification_flow_body.go index f4e995d222c3..9065bfdbc58e 100644 --- a/internal/client-go/model_update_verification_flow_body.go +++ b/internal/client-go/model_update_verification_flow_body.go @@ -39,44 +39,62 @@ func UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(v *UpdateV // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateVerificationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateVerificationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithCodeMethod) - if err == nil { - jsonUpdateVerificationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithCodeMethod) - if string(jsonUpdateVerificationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateVerificationFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithLinkMethod) - if err == nil { - jsonUpdateVerificationFlowWithLinkMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithLinkMethod) - if string(jsonUpdateVerificationFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateVerificationFlowWithCodeMethod = nil - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'updateVerificationFlowWithCodeMethod' + if jsonDict["method"] == "updateVerificationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateVerificationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateVerificationFlowBody)") + // check if the discriminator value is 'updateVerificationFlowWithLinkMethod' + if jsonDict["method"] == "updateVerificationFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_continue_with.go b/internal/httpclient/model_continue_with.go index ee84e74692fb..9e97dbf479e7 100644 --- a/internal/httpclient/model_continue_with.go +++ b/internal/httpclient/model_continue_with.go @@ -55,72 +55,110 @@ func ContinueWithVerificationUiAsContinueWith(v *ContinueWithVerificationUi) Con // Unmarshal JSON data into one of the pointers in the struct func (dst *ContinueWith) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into ContinueWithRecoveryUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithRecoveryUi) - if err == nil { - jsonContinueWithRecoveryUi, _ := json.Marshal(dst.ContinueWithRecoveryUi) - if string(jsonContinueWithRecoveryUi) == "{}" { // empty struct - dst.ContinueWithRecoveryUi = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'set_ory_session_token' + if jsonDict["action"] == "set_ory_session_token" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match } else { - match++ + dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) } - } else { - dst.ContinueWithRecoveryUi = nil } - // try to unmarshal data into ContinueWithSetOrySessionToken - err = newStrictDecoder(data).Decode(&dst.ContinueWithSetOrySessionToken) - if err == nil { - jsonContinueWithSetOrySessionToken, _ := json.Marshal(dst.ContinueWithSetOrySessionToken) - if string(jsonContinueWithSetOrySessionToken) == "{}" { // empty struct - dst.ContinueWithSetOrySessionToken = nil + // check if the discriminator value is 'show_recovery_ui' + if jsonDict["action"] == "show_recovery_ui" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithSetOrySessionToken = nil } - // try to unmarshal data into ContinueWithSettingsUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithSettingsUi) - if err == nil { - jsonContinueWithSettingsUi, _ := json.Marshal(dst.ContinueWithSettingsUi) - if string(jsonContinueWithSettingsUi) == "{}" { // empty struct - dst.ContinueWithSettingsUi = nil + // check if the discriminator value is 'show_settings_ui' + if jsonDict["action"] == "show_settings_ui" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match } else { - match++ + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) } - } else { - dst.ContinueWithSettingsUi = nil } - // try to unmarshal data into ContinueWithVerificationUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithVerificationUi) - if err == nil { - jsonContinueWithVerificationUi, _ := json.Marshal(dst.ContinueWithVerificationUi) - if string(jsonContinueWithVerificationUi) == "{}" { // empty struct + // check if the discriminator value is 'show_verification_ui' + if jsonDict["action"] == "show_verification_ui" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithRecoveryUi' + if jsonDict["action"] == "continueWithRecoveryUi" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithVerificationUi = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.ContinueWithRecoveryUi = nil - dst.ContinueWithSetOrySessionToken = nil - dst.ContinueWithSettingsUi = nil - dst.ContinueWithVerificationUi = nil + // check if the discriminator value is 'continueWithSetOrySessionToken' + if jsonDict["action"] == "continueWithSetOrySessionToken" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match + } else { + dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithSettingsUi' + if jsonDict["action"] == "continueWithSettingsUi" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match + } else { + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(ContinueWith)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(ContinueWith)") + // check if the discriminator value is 'continueWithVerificationUi' + if jsonDict["action"] == "continueWithVerificationUi" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { + dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_ui_node_anchor_attributes.go b/internal/httpclient/model_ui_node_anchor_attributes.go index 15cb492fe8eb..ad2cc992a119 100644 --- a/internal/httpclient/model_ui_node_anchor_attributes.go +++ b/internal/httpclient/model_ui_node_anchor_attributes.go @@ -21,7 +21,7 @@ type UiNodeAnchorAttributes struct { Href string `json:"href"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Title UiText `json:"title"` } diff --git a/internal/httpclient/model_ui_node_attributes.go b/internal/httpclient/model_ui_node_attributes.go index 347be6f44a72..510dc20f8564 100644 --- a/internal/httpclient/model_ui_node_attributes.go +++ b/internal/httpclient/model_ui_node_attributes.go @@ -63,86 +63,134 @@ func UiNodeTextAttributesAsUiNodeAttributes(v *UiNodeTextAttributes) UiNodeAttri // Unmarshal JSON data into one of the pointers in the struct func (dst *UiNodeAttributes) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UiNodeAnchorAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeAnchorAttributes) - if err == nil { - jsonUiNodeAnchorAttributes, _ := json.Marshal(dst.UiNodeAnchorAttributes) - if string(jsonUiNodeAnchorAttributes) == "{}" { // empty struct - dst.UiNodeAnchorAttributes = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'a' + if jsonDict["node_type"] == "a" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match } else { - match++ + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) } - } else { - dst.UiNodeAnchorAttributes = nil } - // try to unmarshal data into UiNodeImageAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeImageAttributes) - if err == nil { - jsonUiNodeImageAttributes, _ := json.Marshal(dst.UiNodeImageAttributes) - if string(jsonUiNodeImageAttributes) == "{}" { // empty struct - dst.UiNodeImageAttributes = nil + // check if the discriminator value is 'img' + if jsonDict["node_type"] == "img" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeImageAttributes = nil } - // try to unmarshal data into UiNodeInputAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeInputAttributes) - if err == nil { - jsonUiNodeInputAttributes, _ := json.Marshal(dst.UiNodeInputAttributes) - if string(jsonUiNodeInputAttributes) == "{}" { // empty struct - dst.UiNodeInputAttributes = nil + // check if the discriminator value is 'input' + if jsonDict["node_type"] == "input" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match } else { - match++ + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) } - } else { - dst.UiNodeInputAttributes = nil } - // try to unmarshal data into UiNodeScriptAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeScriptAttributes) - if err == nil { - jsonUiNodeScriptAttributes, _ := json.Marshal(dst.UiNodeScriptAttributes) - if string(jsonUiNodeScriptAttributes) == "{}" { // empty struct - dst.UiNodeScriptAttributes = nil + // check if the discriminator value is 'script' + if jsonDict["node_type"] == "script" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match } else { - match++ + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) } - } else { - dst.UiNodeScriptAttributes = nil } - // try to unmarshal data into UiNodeTextAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeTextAttributes) - if err == nil { - jsonUiNodeTextAttributes, _ := json.Marshal(dst.UiNodeTextAttributes) - if string(jsonUiNodeTextAttributes) == "{}" { // empty struct + // check if the discriminator value is 'text' + if jsonDict["node_type"] == "text" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeAnchorAttributes' + if jsonDict["node_type"] == "uiNodeAnchorAttributes" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match + } else { + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeImageAttributes' + if jsonDict["node_type"] == "uiNodeImageAttributes" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeTextAttributes = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UiNodeAnchorAttributes = nil - dst.UiNodeImageAttributes = nil - dst.UiNodeInputAttributes = nil - dst.UiNodeScriptAttributes = nil - dst.UiNodeTextAttributes = nil + // check if the discriminator value is 'uiNodeInputAttributes' + if jsonDict["node_type"] == "uiNodeInputAttributes" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match + } else { + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UiNodeAttributes)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UiNodeAttributes)") + // check if the discriminator value is 'uiNodeScriptAttributes' + if jsonDict["node_type"] == "uiNodeScriptAttributes" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match + } else { + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeTextAttributes' + if jsonDict["node_type"] == "uiNodeTextAttributes" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { + dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_ui_node_image_attributes.go b/internal/httpclient/model_ui_node_image_attributes.go index 5b300b9548bd..6eef160e3d67 100644 --- a/internal/httpclient/model_ui_node_image_attributes.go +++ b/internal/httpclient/model_ui_node_image_attributes.go @@ -21,7 +21,7 @@ type UiNodeImageAttributes struct { Height int64 `json:"height"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // The image's source URL. format: uri Src string `json:"src"` diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go index fbf7e0f1b04e..b373dda7ccfd 100644 --- a/internal/httpclient/model_ui_node_input_attributes.go +++ b/internal/httpclient/model_ui_node_input_attributes.go @@ -24,7 +24,7 @@ type UiNodeInputAttributes struct { Label *UiText `json:"label,omitempty"` // The input's element name. Name string `json:"name"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Onclick *string `json:"onclick,omitempty"` diff --git a/internal/httpclient/model_ui_node_script_attributes.go b/internal/httpclient/model_ui_node_script_attributes.go index 21dca70cbe86..d867c1c66dba 100644 --- a/internal/httpclient/model_ui_node_script_attributes.go +++ b/internal/httpclient/model_ui_node_script_attributes.go @@ -25,7 +25,7 @@ type UiNodeScriptAttributes struct { Id string `json:"id"` // The script's integrity hash Integrity string `json:"integrity"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // Nonce for CSP A nonce you may want to use to improve your Content Security Policy. You do not have to use this value but if you want to improve your CSP policies you may use it. You can also choose to use your own nonce value! Nonce string `json:"nonce"` diff --git a/internal/httpclient/model_ui_node_text_attributes.go b/internal/httpclient/model_ui_node_text_attributes.go index 6199187b5d75..93e0f8314191 100644 --- a/internal/httpclient/model_ui_node_text_attributes.go +++ b/internal/httpclient/model_ui_node_text_attributes.go @@ -19,7 +19,7 @@ import ( type UiNodeTextAttributes struct { // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Text UiText `json:"text"` } diff --git a/internal/httpclient/model_update_login_flow_body.go b/internal/httpclient/model_update_login_flow_body.go index 36033328e78d..ac3e4f503292 100644 --- a/internal/httpclient/model_update_login_flow_body.go +++ b/internal/httpclient/model_update_login_flow_body.go @@ -71,100 +71,158 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateLoginFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) - if err == nil { - jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) - if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) - if err == nil { - jsonUpdateLoginFlowWithLookupSecretMethod, _ := json.Marshal(dst.UpdateLoginFlowWithLookupSecretMethod) - if string(jsonUpdateLoginFlowWithLookupSecretMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithLookupSecretMethod = nil + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithLookupSecretMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithOidcMethod) - if err == nil { - jsonUpdateLoginFlowWithOidcMethod, _ := json.Marshal(dst.UpdateLoginFlowWithOidcMethod) - if string(jsonUpdateLoginFlowWithOidcMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithOidcMethod = nil + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithPasswordMethod) - if err == nil { - jsonUpdateLoginFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateLoginFlowWithPasswordMethod) - if string(jsonUpdateLoginFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithTotpMethod) - if err == nil { - jsonUpdateLoginFlowWithTotpMethod, _ := json.Marshal(dst.UpdateLoginFlowWithTotpMethod) - if string(jsonUpdateLoginFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateLoginFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateLoginFlowWithWebAuthnMethod) - if string(jsonUpdateLoginFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithCodeMethod' + if jsonDict["method"] == "updateLoginFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateLoginFlowWithCodeMethod = nil - dst.UpdateLoginFlowWithLookupSecretMethod = nil - dst.UpdateLoginFlowWithOidcMethod = nil - dst.UpdateLoginFlowWithPasswordMethod = nil - dst.UpdateLoginFlowWithTotpMethod = nil - dst.UpdateLoginFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod' + if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match + } else { + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateLoginFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateLoginFlowBody)") + // check if the discriminator value is 'updateLoginFlowWithOidcMethod' + if jsonDict["method"] == "updateLoginFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match + } else { + dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithPasswordMethod' + if jsonDict["method"] == "updateLoginFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithTotpMethod' + if jsonDict["method"] == "updateLoginFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match + } else { + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateLoginFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_update_recovery_flow_body.go b/internal/httpclient/model_update_recovery_flow_body.go index 6eea1d5d6b6a..b0f6de861b4f 100644 --- a/internal/httpclient/model_update_recovery_flow_body.go +++ b/internal/httpclient/model_update_recovery_flow_body.go @@ -39,44 +39,62 @@ func UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryF // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRecoveryFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRecoveryFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithCodeMethod) - if err == nil { - jsonUpdateRecoveryFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithCodeMethod) - if string(jsonUpdateRecoveryFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRecoveryFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithLinkMethod) - if err == nil { - jsonUpdateRecoveryFlowWithLinkMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithLinkMethod) - if string(jsonUpdateRecoveryFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRecoveryFlowWithCodeMethod = nil - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'updateRecoveryFlowWithCodeMethod' + if jsonDict["method"] == "updateRecoveryFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRecoveryFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRecoveryFlowBody)") + // check if the discriminator value is 'updateRecoveryFlowWithLinkMethod' + if jsonDict["method"] == "updateRecoveryFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go index 0e36a95f635f..7272ea1ace1a 100644 --- a/internal/httpclient/model_update_registration_flow_body.go +++ b/internal/httpclient/model_update_registration_flow_body.go @@ -55,72 +55,110 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) - if err == nil { - jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) - if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) - if err == nil { - jsonUpdateRegistrationFlowWithOidcMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithOidcMethod) - if string(jsonUpdateRegistrationFlowWithOidcMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithOidcMethod = nil + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithPasswordMethod) - if err == nil { - jsonUpdateRegistrationFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithPasswordMethod) - if string(jsonUpdateRegistrationFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateRegistrationFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithWebAuthnMethod) - if string(jsonUpdateRegistrationFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithCodeMethod' + if jsonDict["method"] == "updateRegistrationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRegistrationFlowWithCodeMethod = nil - dst.UpdateRegistrationFlowWithOidcMethod = nil - dst.UpdateRegistrationFlowWithPasswordMethod = nil - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateRegistrationFlowWithOidcMethod' + if jsonDict["method"] == "updateRegistrationFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithPasswordMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRegistrationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRegistrationFlowBody)") + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_update_settings_flow_body.go b/internal/httpclient/model_update_settings_flow_body.go index 064ff9b771dd..e2aec380a586 100644 --- a/internal/httpclient/model_update_settings_flow_body.go +++ b/internal/httpclient/model_update_settings_flow_body.go @@ -71,100 +71,158 @@ func UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(v *UpdateSetti // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateSettingsFlowWithLookupMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithLookupMethod) - if err == nil { - jsonUpdateSettingsFlowWithLookupMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithLookupMethod) - if string(jsonUpdateSettingsFlowWithLookupMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithLookupMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithLookupMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithOidcMethod) - if err == nil { - jsonUpdateSettingsFlowWithOidcMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithOidcMethod) - if string(jsonUpdateSettingsFlowWithOidcMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithOidcMethod = nil + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithPasswordMethod) - if err == nil { - jsonUpdateSettingsFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithPasswordMethod) - if string(jsonUpdateSettingsFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithProfileMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithProfileMethod) - if err == nil { - jsonUpdateSettingsFlowWithProfileMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithProfileMethod) - if string(jsonUpdateSettingsFlowWithProfileMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithProfileMethod = nil + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithProfileMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithTotpMethod) - if err == nil { - jsonUpdateSettingsFlowWithTotpMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithTotpMethod) - if string(jsonUpdateSettingsFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateSettingsFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithWebAuthnMethod) - if string(jsonUpdateSettingsFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithLookupMethod' + if jsonDict["method"] == "updateSettingsFlowWithLookupMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateSettingsFlowWithLookupMethod = nil - dst.UpdateSettingsFlowWithOidcMethod = nil - dst.UpdateSettingsFlowWithPasswordMethod = nil - dst.UpdateSettingsFlowWithProfileMethod = nil - dst.UpdateSettingsFlowWithTotpMethod = nil - dst.UpdateSettingsFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateSettingsFlowWithOidcMethod' + if jsonDict["method"] == "updateSettingsFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateSettingsFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateSettingsFlowBody)") + // check if the discriminator value is 'updateSettingsFlowWithPasswordMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithProfileMethod' + if jsonDict["method"] == "updateSettingsFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithTotpMethod' + if jsonDict["method"] == "updateSettingsFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateSettingsFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_update_verification_flow_body.go b/internal/httpclient/model_update_verification_flow_body.go index f4e995d222c3..9065bfdbc58e 100644 --- a/internal/httpclient/model_update_verification_flow_body.go +++ b/internal/httpclient/model_update_verification_flow_body.go @@ -39,44 +39,62 @@ func UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(v *UpdateV // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateVerificationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateVerificationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithCodeMethod) - if err == nil { - jsonUpdateVerificationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithCodeMethod) - if string(jsonUpdateVerificationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateVerificationFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithLinkMethod) - if err == nil { - jsonUpdateVerificationFlowWithLinkMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithLinkMethod) - if string(jsonUpdateVerificationFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateVerificationFlowWithCodeMethod = nil - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'updateVerificationFlowWithCodeMethod' + if jsonDict["method"] == "updateVerificationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateVerificationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateVerificationFlowBody)") + // check if the discriminator value is 'updateVerificationFlowWithLinkMethod' + if jsonDict["method"] == "updateVerificationFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/spec/api.json b/spec/api.json index 4a35eaeb87bc..93437704b045 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2213,8 +2213,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "title": { "$ref": "#/components/schemas/uiText" @@ -2271,8 +2279,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "src": { "description": "The image's source URL.\n\nformat: uri", @@ -2322,8 +2338,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", @@ -2402,8 +2426,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "nonce": { "description": "Nonce for CSP\n\nA nonce you may want to use to improve your Content Security Policy.\nYou do not have to use this value but if you want to improve your CSP\npolicies you may use it. You can also choose to use your own nonce value!", @@ -2443,8 +2475,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "text": { "$ref": "#/components/schemas/uiText" diff --git a/spec/swagger.json b/spec/swagger.json index 4621ef4fbfca..f28bc3023c56 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5316,8 +5316,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "title": { "$ref": "#/definitions/uiText" @@ -5349,8 +5357,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "src": { "description": "The image's source URL.\n\nformat: uri", @@ -5398,8 +5414,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", @@ -5483,8 +5507,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "nonce": { "description": "Nonce for CSP\n\nA nonce you may want to use to improve your Content Security Policy.\nYou do not have to use this value but if you want to improve your CSP\npolicies you may use it. You can also choose to use your own nonce value!", @@ -5518,8 +5550,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "text": { "$ref": "#/definitions/uiText" diff --git a/ui/node/attributes.go b/ui/node/attributes.go index daf56c9fc675..9611b5828dff 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -101,7 +101,7 @@ type InputAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // ImageAttributes represents the attributes of an image node. @@ -133,7 +133,7 @@ type ImageAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "img". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // AnchorAttributes represents the attributes of an anchor node. @@ -160,7 +160,7 @@ type AnchorAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "a". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // TextAttributes represents the attributes of a text node. @@ -181,7 +181,7 @@ type TextAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "text". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // ScriptAttributes represent script nodes which load javascript. @@ -236,7 +236,7 @@ type ScriptAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "script". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } var ( diff --git a/ui/node/node.go b/ui/node/node.go index c4e9e05519f8..e08295b827f4 100644 --- a/ui/node/node.go +++ b/ui/node/node.go @@ -358,23 +358,23 @@ func (n *Node) UnmarshalJSON(data []byte) error { switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) { case Text: attr = &TextAttributes{ - NodeType: string(Text), + NodeType: Text, } case Input: attr = &InputAttributes{ - NodeType: string(Input), + NodeType: Input, } case Anchor: attr = &AnchorAttributes{ - NodeType: string(Anchor), + NodeType: Anchor, } case Image: attr = &ImageAttributes{ - NodeType: string(Image), + NodeType: Image, } case Script: attr = &ScriptAttributes{ - NodeType: string(Script), + NodeType: Script, } default: return fmt.Errorf("unexpected node type: %s", t) @@ -401,19 +401,19 @@ func (n *Node) MarshalJSON() ([]byte, error) { switch attr := n.Attributes.(type) { case *TextAttributes: t = Text - attr.NodeType = string(Text) + attr.NodeType = Text case *InputAttributes: t = Input - attr.NodeType = string(Input) + attr.NodeType = Input case *AnchorAttributes: t = Anchor - attr.NodeType = string(Anchor) + attr.NodeType = Anchor case *ImageAttributes: t = Image - attr.NodeType = string(Image) + attr.NodeType = Image case *ScriptAttributes: t = Script - attr.NodeType = string(Script) + attr.NodeType = Script default: return nil, errors.WithStack(fmt.Errorf("unknown node type: %T", n.Attributes)) } From fa5a1129f7b0c45d10e804f80e7c505c2dd88e42 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:26:32 +0000 Subject: [PATCH 041/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cba2ae2b63c..f42b7a20bf31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-03-12)](#2024-03-12) +- [ (2024-03-15)](#2024-03-15) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-12) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-15) ## Breaking Changes @@ -359,6 +359,9 @@ defaults to `false`. - Prevent SMTP URL leak on unparsable URL ([#3770](https://github.com/ory/kratos/issues/3770)) ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) +- **sdk:** Improve discriminators for node and Go + ([#3821](https://github.com/ory/kratos/issues/3821)) + ([9ddf7cc](https://github.com/ory/kratos/commit/9ddf7cc7c52313c4ee13ccdc2886ad94b5d1317f)) - Show error page on identity mismatch ([#3790](https://github.com/ory/kratos/issues/3790)) ([e6db689](https://github.com/ory/kratos/commit/e6db689e0de41067e6e78889c3dab9637a96236e)) From 3d9ba5df85e0d0c4d8002365987e536b37678104 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:58:55 +0100 Subject: [PATCH 042/262] feat: use authenticate endpoint for x (#3833) Improves the "Log in with X" experience by not asking the user to re-authenticate every time. --- internal/client-go/go.sum | 1 + selfservice/strategy/oidc/provider_x.go | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/strategy/oidc/provider_x.go b/selfservice/strategy/oidc/provider_x.go index 060ba58a6303..f58dbd48182f 100644 --- a/selfservice/strategy/oidc/provider_x.go +++ b/selfservice/strategy/oidc/provider_x.go @@ -9,6 +9,8 @@ import ( "fmt" "net/http" + "github.com/ory/x/otelx" + "github.com/dghubble/oauth1" "github.com/dghubble/oauth1/twitter" "github.com/pkg/errors" @@ -54,7 +56,10 @@ func (p *ProviderX) ExchangeToken(ctx context.Context, req *http.Request) (*oaut return oauth1.NewToken(accessToken, accessSecret), nil } -func (p *ProviderX) AuthURL(ctx context.Context, state string) (string, error) { +func (p *ProviderX) AuthURL(ctx context.Context, state string) (_ string, err error) { + ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.fetch") + defer otelx.End(span, &err) + c := p.OAuth1(ctx) // We need to cheat so that callback validates on return @@ -62,12 +67,14 @@ func (p *ProviderX) AuthURL(ctx context.Context, state string) (string, error) { requestToken, _, err := c.RequestToken() if err != nil { - return "", errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`Unable to sign in with X because the OAuth1 request token could not be initialized.`)) + span.RecordError(err) + return "", errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 request token could not be initialized: %s`, err)) } authzURL, err := c.AuthorizationURL(requestToken) if err != nil { - return "", errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`Unable to sign in with X because the OAuth1 authorization URL could not be parsed.`)) + span.RecordError(err) + return "", errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 authorization URL could not be parsed: %s`, err)) } return authzURL.String(), nil @@ -85,7 +92,7 @@ func (p *ProviderX) OAuth1(ctx context.Context) *oauth1.Config { return &oauth1.Config{ ConsumerKey: p.config.ClientID, ConsumerSecret: p.config.ClientSecret, - Endpoint: twitter.AuthorizeEndpoint, + Endpoint: twitter.AuthenticateEndpoint, CallbackURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)), } } From 8ebdfd2fb207bddecb2b98d0faf41adbc8b69d15 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:00:24 +0000 Subject: [PATCH 043/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 43e4eadce7fa6e66bf1f9c03136d141bffd3094f Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 21 Mar 2024 03:11:20 -0700 Subject: [PATCH 044/262] feat: add verification hook to login flow (#3829) --- driver/registry_default_hooks.go | 2 + embedx/config.schema.json | 22 +++ selfservice/flow/login/flow.go | 16 ++ selfservice/flow/login/hook.go | 67 ++++--- selfservice/flow/login/session.go | 13 +- selfservice/hook/hooks.go | 1 + selfservice/hook/verification.go | 27 ++- selfservice/hook/verification_test.go | 275 +++++++++++++++++--------- 8 files changed, 291 insertions(+), 132 deletions(-) diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index 1c436e932d4e..05b9a10f3d98 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -76,6 +76,8 @@ func (m *RegistryDefault) getHooks(credentialsType string, configs []config.Self i = append(i, m.HookShowVerificationUI()) case hook.KeyTwoStepRegistration: i = append(i, m.HookTwoStepRegistration()) + case hook.KeyVerifier: + i = append(i, m.HookVerifier()) default: var found bool for name, m := range m.injectedSelfserviceHooks { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 473f0bf514fd..15cc950fbf8e 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -75,6 +75,16 @@ "additionalProperties": false, "required": ["hook"] }, + "selfServiceVerificationHook": { + "type": "object", + "properties": { + "hook": { + "const": "verification" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, "selfServiceShowVerificationUIHook": { "type": "object", "properties": { @@ -735,6 +745,12 @@ }, { "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" } ] }, @@ -893,6 +909,12 @@ { "$ref": "#/definitions/selfServiceRequireVerifiedAddressHook" }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" + }, { "$ref": "#/definitions/b2bSSOHook" } diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index a6bb0d55d60b..9e91deafc678 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -145,6 +145,12 @@ type Flow struct { // // required: false TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` + + // Contains a list of actions, that could follow this flow + // + // It can, for example, contain a reference to the verification flow, created as part of the user's + // registration. + ContinueWithItems []flow.ContinueWith `json:"-" db:"-" faker:"-" ` } var _ flow.Flow = new(Flow) @@ -301,3 +307,13 @@ func (f *Flow) SetState(state flow.State) { func (t *Flow) GetTransientPayload() json.RawMessage { return t.TransientPayload } + +var _ flow.FlowWithContinueWith = new(Flow) + +func (f *Flow) AddContinueWith(c flow.ContinueWith) { + f.ContinueWithItems = append(f.ContinueWithItems, c) +} + +func (f *Flow) ContinueWith() []flow.ContinueWith { + return f.ContinueWithItems +} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 418e9237df5c..595fdbff7936 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -122,7 +122,7 @@ func (e *HookExecutor) PostLoginHook( w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, - a *Flow, + f *Flow, i *identity.Identity, s *session.Session, provider string, @@ -132,7 +132,7 @@ func (e *HookExecutor) PostLoginHook( r = r.WithContext(ctx) defer otelx.End(span, &err) - if err := e.maybeLinkCredentials(r.Context(), s, i, a); err != nil { + if err := e.maybeLinkCredentials(r.Context(), s, i, f); err != nil { return err } @@ -144,11 +144,11 @@ func (e *HookExecutor) PostLoginHook( // Verify the redirect URL before we do any other processing. returnTo, err := x.SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(r.Context()), - x.SecureRedirectReturnTo(a.ReturnTo), - x.SecureRedirectUseSourceURL(a.RequestURL), + x.SecureRedirectReturnTo(f.ReturnTo), + x.SecureRedirectUseSourceURL(f.RequestURL), x.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(r.Context())), x.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())), - x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(r.Context(), a.Active.String())), + x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(r.Context(), f.Active.String())), ) if err != nil { return err @@ -165,38 +165,38 @@ func (e *HookExecutor) PostLoginHook( e.d.Logger(). WithRequest(r). WithField("identity_id", i.ID). - WithField("flow_method", a.Active). + WithField("flow_method", f.Active). Debug("Running ExecuteLoginPostHook.") - for k, executor := range e.d.PostLoginHooks(r.Context(), a.Active) { - if err := executor.ExecuteLoginPostHook(w, r, g, a, s); err != nil { + for k, executor := range e.d.PostLoginHooks(r.Context(), f.Active) { + if err := executor.ExecuteLoginPostHook(w, r, g, f, s); err != nil { if errors.Is(err, ErrHookAbortFlow) { e.d.Logger(). WithRequest(r). WithField("executor", fmt.Sprintf("%T", executor)). WithField("executor_position", k). - WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), a.Active))). + WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), f.Active))). WithField("identity_id", i.ID). - WithField("flow_method", a.Active). + WithField("flow_method", f.Active). Debug("A ExecuteLoginPostHook hook aborted early.") span.SetAttributes(attribute.String("redirect_reason", "aborted by hook"), attribute.String("executor", fmt.Sprintf("%T", executor))) return nil } - return e.handleLoginError(w, r, g, a, i, err) + return e.handleLoginError(w, r, g, f, i, err) } e.d.Logger(). WithRequest(r). WithField("executor", fmt.Sprintf("%T", executor)). WithField("executor_position", k). - WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), a.Active))). + WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), f.Active))). WithField("identity_id", i.ID). - WithField("flow_method", a.Active). + WithField("flow_method", f.Active). Debug("ExecuteLoginPostHook completed successfully.") } - if a.Type == flow.TypeAPI { + if f.Type == flow.TypeAPI { span.SetAttributes(attribute.String("flow_type", string(flow.TypeAPI))) if err := e.d.SessionPersister().UpsertSession(r.Context(), s); err != nil { return errors.WithStack(err) @@ -210,23 +210,27 @@ func (e *HookExecutor) PostLoginHook( span.AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ SessionID: s.ID, IdentityID: i.ID, - FlowType: string(a.Type), - RequestedAAL: string(a.RequestedAAL), - IsRefresh: a.Refresh, - Method: a.Active.String(), + FlowType: string(f.Type), + RequestedAAL: string(f.RequestedAAL), + IsRefresh: f.Refresh, + Method: f.Active.String(), SSOProvider: provider, })) - if a.IDToken != "" { + if f.IDToken != "" { // We don't want to redirect with the code, if the flow was submitted with an ID token. // This is the case for Sign in with native Apple SDK or Google SDK. - } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, g); err != nil { + } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, f, s.ID, g); err != nil { return errors.WithStack(err) } else if handled { return nil } - response := &APIFlowResponse{Session: s, Token: s.Token} - if required, _ := e.requiresAAL2(r, classified, a); required { + response := &APIFlowResponse{ + Session: s, + Token: s.Token, + ContinueWith: f.ContinueWith(), + } + if required, _ := e.requiresAAL2(r, classified, f); required { // If AAL is not satisfied, we omit the identity to preserve the user's privacy in case of a phishing attack. response.Session.Identity = nil } @@ -247,7 +251,7 @@ func (e *HookExecutor) PostLoginHook( trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ SessionID: s.ID, - IdentityID: i.ID, FlowType: string(a.Type), RequestedAAL: string(a.RequestedAAL), IsRefresh: a.Refresh, Method: a.Active.String(), + IdentityID: i.ID, FlowType: string(f.Type), RequestedAAL: string(f.RequestedAAL), IsRefresh: f.Refresh, Method: f.Active.String(), SSOProvider: provider, })) @@ -258,7 +262,7 @@ func (e *HookExecutor) PostLoginHook( s.Token = "" // If we detect that whoami would require a higher AAL, we redirect! - if _, err := e.requiresAAL2(r, s, a); err != nil { + if _, err := e.requiresAAL2(r, s, f); err != nil { if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { span.SetAttributes(attribute.String("return_to", aalErr.RedirectTo), attribute.String("redirect_reason", "requires aal2")) e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(aalErr.RedirectTo)) @@ -269,10 +273,10 @@ func (e *HookExecutor) PostLoginHook( // If Kratos is used as a Hydra login provider, we need to redirect back to Hydra by returning a 422 status // with the post login challenge URL as the body. - if a.OAuth2LoginChallenge != "" { + if f.OAuth2LoginChallenge != "" { postChallengeURL, err := e.d.Hydra().AcceptLoginRequest(r.Context(), hydra.AcceptLoginRequestParams{ - LoginChallenge: string(a.OAuth2LoginChallenge), + LoginChallenge: string(f.OAuth2LoginChallenge), IdentityID: i.ID.String(), SessionID: s.ID.String(), AuthenticationMethods: s.AMR, @@ -285,13 +289,16 @@ func (e *HookExecutor) PostLoginHook( return nil } - response := &APIFlowResponse{Session: s} + response := &APIFlowResponse{ + Session: s, + ContinueWith: f.ContinueWith(), + } e.d.Writer().Write(w, r, response) return nil } // If we detect that whoami would require a higher AAL, we redirect! - if _, err := e.requiresAAL2(r, s, a); err != nil { + if _, err := e.requiresAAL2(r, s, f); err != nil { if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { http.Redirect(w, r, aalErr.RedirectTo, http.StatusSeeOther) return nil @@ -300,10 +307,10 @@ func (e *HookExecutor) PostLoginHook( } finalReturnTo := returnTo.String() - if a.OAuth2LoginChallenge != "" { + if f.OAuth2LoginChallenge != "" { rt, err := e.d.Hydra().AcceptLoginRequest(r.Context(), hydra.AcceptLoginRequestParams{ - LoginChallenge: string(a.OAuth2LoginChallenge), + LoginChallenge: string(f.OAuth2LoginChallenge), IdentityID: i.ID.String(), SessionID: s.ID.String(), AuthenticationMethods: s.AMR, diff --git a/selfservice/flow/login/session.go b/selfservice/flow/login/session.go index 8b99d037e15a..35e2aff87b38 100644 --- a/selfservice/flow/login/session.go +++ b/selfservice/flow/login/session.go @@ -3,7 +3,10 @@ package login -import "github.com/ory/kratos/session" +import ( + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" +) // The Response for Login Flows via API // @@ -26,4 +29,12 @@ type APIFlowResponse struct { // // required: true Session *session.Session `json:"session"` + + // Contains a list of actions, that could follow this flow + // + // It can, for example, this will contain a reference to the verification flow, created as part of the user's + // registration or the token of the session. + // + // required: false + ContinueWith []flow.ContinueWith `json:"continue_with"` } diff --git a/selfservice/hook/hooks.go b/selfservice/hook/hooks.go index 3b2060c9c6a1..c272f954087f 100644 --- a/selfservice/hook/hooks.go +++ b/selfservice/hook/hooks.go @@ -10,4 +10,5 @@ const ( KeyAddressVerifier = "require_verified_address" KeyVerificationUI = "show_verification_ui" KeyTwoStepRegistration = "two_step_registration" + KeyVerifier = "verification" ) diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index c75cf1ce073b..ef4bf2e3f16a 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -12,10 +12,12 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/otelx" ) @@ -23,6 +25,7 @@ import ( var ( _ registration.PostHookPostPersistExecutor = new(Verifier) _ settings.PostHookPostPersistExecutor = new(Verifier) + _ login.PostHookExecutor = new(Verifier) ) type ( @@ -34,6 +37,7 @@ type ( verification.FlowPersistenceProvider identity.PrivilegedPoolProvider x.WriterProvider + x.TracingProvider } Verifier struct { r verifierDependencies @@ -61,6 +65,18 @@ func (e *Verifier) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http }) } +func (e *Verifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, f *login.Flow, s *session.Session) (err error) { + ctx, span := e.r.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.Verifier.ExecuteLoginPostHook") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + if f.RequestedAAL != identity.AuthenticatorAssuranceLevel1 { + span.AddEvent("Skipping verification hook because AAL is not 1") + return nil + } + + return e.do(w, r.WithContext(ctx), s.Identity, f, nil) +} + func (e *Verifier) do( w http.ResponseWriter, r *http.Request, @@ -78,11 +94,16 @@ func (e *Verifier) do( } isBrowserFlow := f.GetType() == flow.TypeBrowser - isRegistrationFlow := f.GetFlowName() == flow.RegistrationFlow + isRegistrationOrLoginFlow := f.GetFlowName() == flow.RegistrationFlow for k := range i.VerifiableAddresses { address := &i.VerifiableAddresses[k] - if address.Status != identity.VerifiableAddressStatusPending { + if isRegistrationOrLoginFlow && address.Verified { + continue + } else if !isRegistrationOrLoginFlow && address.Status != identity.VerifiableAddressStatusPending { + // In case of the settings flow, we only want to create a new verification flow if there is no pending + // verification flow for the address. Otherwise, we would create a new verification flow for each setting, + // even if the address did not change. continue } @@ -90,7 +111,7 @@ func (e *Verifier) do( // TODO: this is pretty ugly, we should probably have a better way to handle CSRF tokens here. if isBrowserFlow { - if isRegistrationFlow { + if isRegistrationOrLoginFlow { // If this hook is executed from a registration flow, we need to regenerate the CSRF token. csrf = e.r.CSRFHandler().RegenerateToken(w, r) } else { diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 652df7bec61c..5e815fa4d4a4 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -12,6 +12,7 @@ import ( "github.com/ory/kratos/courier" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/ui/node" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,11 +21,13 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" "github.com/ory/kratos/x" + "github.com/ory/x/pointerx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" ) @@ -32,109 +35,185 @@ import ( func TestVerifier(t *testing.T) { ctx := context.Background() u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} - for k, hf := range map[string]func(*hook.Verifier, *identity.Identity, flow.Flow) error{ - "settings": func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { - return h.ExecuteSettingsPostPersistHook( - httptest.NewRecorder(), u, f.(*settings.Flow), i, &session.Session{ID: x.NewUUID(), Identity: i}) + + for _, tc := range []struct { + name string + execHook func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error + originalFlow func() flow.FlowWithContinueWith + }{ + { + name: "login", + execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { + return h.ExecuteLoginPostHook( + httptest.NewRecorder(), u, node.CodeGroup, f.(*login.Flow), &session.Session{ID: x.NewUUID(), Identity: i}) + }, + originalFlow: func() flow.FlowWithContinueWith { + return &login.Flow{RequestURL: "http://foo.com/login", RequestedAAL: "aal1"} + }, }, - "register": func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { - return h.ExecutePostRegistrationPostPersistHook( - httptest.NewRecorder(), u, f.(*registration.Flow), &session.Session{ID: x.NewUUID(), Identity: i}) + { + name: "registration", + execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { + return h.ExecutePostRegistrationPostPersistHook( + httptest.NewRecorder(), u, f.(*registration.Flow), &session.Session{ID: x.NewUUID(), Identity: i}) + }, + originalFlow: func() flow.FlowWithContinueWith { + return ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"} + }, }, } { - t.Run("name="+k, func(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") - conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") - - i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) - i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh","baz@ory.sh"]}`) - require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) - - actual, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, "foo@ory.sh", actual.Value) - - actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, "bar@ory.sh", actual.Value) - - actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "baz@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, "baz@ory.sh", actual.Value) - - verifiedAt := sqlxx.NullTime(time.Now()) - actual.Status = identity.VerifiableAddressStatusCompleted - actual.Verified = true - actual.VerifiedAt = &verifiedAt - require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(context.Background(), actual)) - - i, err = reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandDefault) - require.NoError(t, err) - - var originalFlow flow.FlowWithContinueWith - switch k { - case "settings": - originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} - case "register": - originalFlow = ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"} - default: - t.FailNow() - } - - h := hook.NewVerifier(reg) - require.NoError(t, hf(h, i, originalFlow)) - assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith()) - assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"}) - vf := originalFlow.ContinueWith()[0] - assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf) - fView := vf.(*flow.ContinueWithVerificationUI).Flow - - expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) - require.NoError(t, err) - require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) - - messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) - require.NoError(t, err) - require.Len(t, messages, 2) - - recipients := make([]string, len(messages)) - for k, m := range messages { - recipients[k] = m.Recipient - } - - assert.Contains(t, recipients, "foo@ory.sh") - assert.Contains(t, recipients, "bar@ory.sh") - // Email to baz@ory.sh is skipped because it is verified already. - assert.NotContains(t, recipients, "baz@ory.sh") - - // these addresses will be marked as sent and won't be sent again by the settings hook - address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) - address2, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, identity.VerifiableAddressStatusSent, address2.Status) - - switch k { - case "settings": - originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} - case "register": - originalFlow = ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"} - default: - t.FailNow() - } - require.NoError(t, hf(h, i, originalFlow)) - - assert.Emptyf(t, originalFlow.ContinueWith(), "%+v", originalFlow.ContinueWith()) - - require.NoError(t, err) - messages, err = reg.CourierPersister().NextMessages(context.Background(), 12) - require.EqualError(t, err, courier.ErrQueueEmpty.Error()) - assert.Len(t, messages, 0) + t.Run("flow="+tc.name, func(t *testing.T) { + t.Run("case=should send out emails for unverified addresses", func(t *testing.T) { + t.Parallel() + originalFlow := tc.originalFlow() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + + h := hook.NewVerifier(reg) + require.NoError(t, tc.execHook(h, i, originalFlow)) + assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith()) + assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"}) + vf := originalFlow.ContinueWith()[0] + assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf) + fView := vf.(*flow.ContinueWithVerificationUI).Flow + + expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) + require.NoError(t, err) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.NoError(t, err) + require.Len(t, messages, 2) + }) + + t.Run("case should skip already verified addresses", func(t *testing.T) { + t.Parallel() + originalFlow := tc.originalFlow() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + h := hook.NewVerifier(reg) + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + for _, address := range i.VerifiableAddresses { + address.Verified = true + address.VerifiedAt = pointerx.Ptr(sqlxx.NullTime(time.Now())) + address.Status = identity.VerifiableAddressStatusCompleted + reg.Persister().UpdateVerifiableAddress(context.Background(), &address) + } + i, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, i.ID, identity.ExpandDefault) + require.NoError(t, err) + + require.NoError(t, tc.execHook(h, i, originalFlow)) + assert.Empty(t, originalFlow.ContinueWith(), "%#ßv", originalFlow.ContinueWith()) + }) }) } + + t.Run("flow=login/case=does not run if aal is not 1", func(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) + + h := hook.NewVerifier(reg) + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + f := &login.Flow{RequestedAAL: "aal2"} + require.NoError(t, h.ExecuteLoginPostHook(httptest.NewRecorder(), u, node.CodeGroup, f, &session.Session{ID: x.NewUUID(), Identity: i})) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.EqualError(t, err, "queue is empty") + require.Len(t, messages, 0) + }) + + t.Run("name=register", func(t *testing.T) { + t.Parallel() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh","baz@ory.sh"]}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + + actual, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "foo@ory.sh", actual.Value) + + actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "bar@ory.sh", actual.Value) + + actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "baz@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "baz@ory.sh", actual.Value) + + verifiedAt := sqlxx.NullTime(time.Now()) + actual.Status = identity.VerifiableAddressStatusCompleted + actual.Verified = true + actual.VerifiedAt = &verifiedAt + require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(context.Background(), actual)) + + i, err = reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandDefault) + require.NoError(t, err) + + originalFlow := &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} + + h := hook.NewVerifier(reg) + require.NoError(t, h.ExecuteSettingsPostPersistHook( + httptest.NewRecorder(), u, originalFlow, i, &session.Session{ID: x.NewUUID(), Identity: i})) + assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith()) + assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"}) + vf := originalFlow.ContinueWith()[0] + assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf) + fView := vf.(*flow.ContinueWithVerificationUI).Flow + + expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) + require.NoError(t, err) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + recipients := make([]string, len(messages)) + for k, m := range messages { + recipients[k] = m.Recipient + } + + assert.Contains(t, recipients, "foo@ory.sh") + assert.Contains(t, recipients, "bar@ory.sh") + // Email to baz@ory.sh is skipped because it is verified already. + assert.NotContains(t, recipients, "baz@ory.sh") + + // these addresses will be marked as sent and won't be sent again by the settings hook + address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) + address2, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, identity.VerifiableAddressStatusSent, address2.Status) + + originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} + + require.NoError(t, h.ExecuteSettingsPostPersistHook( + httptest.NewRecorder(), u, originalFlow, i, &session.Session{ID: x.NewUUID(), Identity: i})) + + assert.Emptyf(t, originalFlow.ContinueWith(), "%+v", originalFlow.ContinueWith()) + + require.NoError(t, err) + messages, err = reg.CourierPersister().NextMessages(context.Background(), 12) + require.EqualError(t, err, courier.ErrQueueEmpty.Error()) + assert.Len(t, messages, 0) + }) } func assertContinueWithAddresses(t *testing.T, cs []flow.ContinueWith, addresses []string) { From 718cb7c3e56b78906c42e4043e41aaf3afb8cfa6 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:13:00 +0000 Subject: [PATCH 045/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- .../model_successful_native_login.go | 39 ++++++++++++++++++- .../model_successful_native_login.go | 39 ++++++++++++++++++- spec/api.json | 7 ++++ spec/swagger.json | 7 ++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/internal/client-go/model_successful_native_login.go b/internal/client-go/model_successful_native_login.go index f87739f19d12..faf59ae906e7 100644 --- a/internal/client-go/model_successful_native_login.go +++ b/internal/client-go/model_successful_native_login.go @@ -17,7 +17,9 @@ import ( // SuccessfulNativeLogin The Response for Login Flows via API type SuccessfulNativeLogin struct { - Session Session `json:"session"` + // Contains a list of actions, that could follow this flow It can, for example, this will contain a reference to the verification flow, created as part of the user's registration or the token of the session. + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + Session Session `json:"session"` // The Session Token A session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization Header: Authorization: bearer ${session-token} The session token is only issued for API flows, not for Browser flows! SessionToken *string `json:"session_token,omitempty"` } @@ -40,6 +42,38 @@ func NewSuccessfulNativeLoginWithDefaults() *SuccessfulNativeLogin { return &this } +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetContinueWith() []ContinueWith { + if o == nil || o.ContinueWith == nil { + var ret []ContinueWith + return ret + } + return o.ContinueWith +} + +// GetContinueWithOk returns a tuple with the ContinueWith field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || o.ContinueWith == nil { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SuccessfulNativeLogin) HasContinueWith() bool { + if o != nil && o.ContinueWith != nil { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SuccessfulNativeLogin) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + // GetSession returns the Session field value func (o *SuccessfulNativeLogin) GetSession() Session { if o == nil { @@ -98,6 +132,9 @@ func (o *SuccessfulNativeLogin) SetSessionToken(v string) { func (o SuccessfulNativeLogin) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.ContinueWith != nil { + toSerialize["continue_with"] = o.ContinueWith + } if true { toSerialize["session"] = o.Session } diff --git a/internal/httpclient/model_successful_native_login.go b/internal/httpclient/model_successful_native_login.go index f87739f19d12..faf59ae906e7 100644 --- a/internal/httpclient/model_successful_native_login.go +++ b/internal/httpclient/model_successful_native_login.go @@ -17,7 +17,9 @@ import ( // SuccessfulNativeLogin The Response for Login Flows via API type SuccessfulNativeLogin struct { - Session Session `json:"session"` + // Contains a list of actions, that could follow this flow It can, for example, this will contain a reference to the verification flow, created as part of the user's registration or the token of the session. + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + Session Session `json:"session"` // The Session Token A session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization Header: Authorization: bearer ${session-token} The session token is only issued for API flows, not for Browser flows! SessionToken *string `json:"session_token,omitempty"` } @@ -40,6 +42,38 @@ func NewSuccessfulNativeLoginWithDefaults() *SuccessfulNativeLogin { return &this } +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetContinueWith() []ContinueWith { + if o == nil || o.ContinueWith == nil { + var ret []ContinueWith + return ret + } + return o.ContinueWith +} + +// GetContinueWithOk returns a tuple with the ContinueWith field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || o.ContinueWith == nil { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SuccessfulNativeLogin) HasContinueWith() bool { + if o != nil && o.ContinueWith != nil { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SuccessfulNativeLogin) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + // GetSession returns the Session field value func (o *SuccessfulNativeLogin) GetSession() Session { if o == nil { @@ -98,6 +132,9 @@ func (o *SuccessfulNativeLogin) SetSessionToken(v string) { func (o SuccessfulNativeLogin) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.ContinueWith != nil { + toSerialize["continue_with"] = o.ContinueWith + } if true { toSerialize["session"] = o.Session } diff --git a/spec/api.json b/spec/api.json index 93437704b045..0f46469df162 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2054,6 +2054,13 @@ "successfulNativeLogin": { "description": "The Response for Login Flows via API", "properties": { + "continue_with": { + "description": "Contains a list of actions, that could follow this flow\n\nIt can, for example, this will contain a reference to the verification flow, created as part of the user's\nregistration or the token of the session.", + "items": { + "$ref": "#/components/schemas/continueWith" + }, + "type": "array" + }, "session": { "$ref": "#/components/schemas/session" }, diff --git a/spec/swagger.json b/spec/swagger.json index f28bc3023c56..37a79fc4f6bb 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5153,6 +5153,13 @@ "session" ], "properties": { + "continue_with": { + "description": "Contains a list of actions, that could follow this flow\n\nIt can, for example, this will contain a reference to the verification flow, created as part of the user's\nregistration or the token of the session.", + "type": "array", + "items": { + "$ref": "#/definitions/continueWith" + } + }, "session": { "$ref": "#/definitions/session" }, From cd92f2a8b0dc221ced82cbedab440afaf399ed61 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:02:04 +0000 Subject: [PATCH 046/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f42b7a20bf31..35f647f783bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-03-15)](#2024-03-15) +- [ (2024-03-21)](#2024-03-21) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-15) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-21) ## Breaking Changes @@ -379,6 +379,9 @@ defaults to `false`. ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) - Add twitter SSO ([#3778](https://github.com/ory/kratos/issues/3778)) ([930fb19](https://github.com/ory/kratos/commit/930fb19842e527e5e9c415efa983b36e02829516)) +- Add verification hook to login flow + ([#3829](https://github.com/ory/kratos/issues/3829)) + ([43e4ead](https://github.com/ory/kratos/commit/43e4eadce7fa6e66bf1f9c03136d141bffd3094f)) - Control edge cache ttl ([#3808](https://github.com/ory/kratos/issues/3808)) ([c9dcce5](https://github.com/ory/kratos/commit/c9dcce5a41137937df1aad7ac81170b443740f88)) - Linkedin v2 provider ([#3804](https://github.com/ory/kratos/issues/3804)) @@ -394,6 +397,12 @@ defaults to `false`. - Send OIDC claim keys to tracing ([#3798](https://github.com/ory/kratos/issues/3798)) ([04390be](https://github.com/ory/kratos/commit/04390bee426befe51af2ee8177afabaa9ce4fa80)) +- Use authenticate endpoint for x + ([#3833](https://github.com/ory/kratos/issues/3833)) + ([3d9ba5d](https://github.com/ory/kratos/commit/3d9ba5df85e0d0c4d8002365987e536b37678104)): + + Improves the "Log in with X" experience by not asking the user to + re-authenticate every time. ### Tests From 8f8fd90304886ecd689a85fc60c4712e47526cdd Mon Sep 17 00:00:00 2001 From: Patrik Date: Fri, 22 Mar 2024 11:21:19 +0100 Subject: [PATCH 047/262] fix: drop trigram index on identifiers (#3827) --- internal/client-go/go.sum | 1 + .../sql/identity/persister_identity.go | 12 ++----- ...000000_drop_identity_search_index.down.sql | 0 ...op_identity_search_index.postgres.down.sql | 4 +++ ...drop_identity_search_index.postgres.up.sql | 1 + ...39000000_drop_identity_search_index.up.sql | 0 x/sql.go | 10 ++++++ x/sql_test.go | 34 +++++++++++++++++++ 8 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.down.sql create mode 100644 persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.down.sql create mode 100644 persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.up.sql create mode 100644 persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.up.sql create mode 100644 x/sql.go create mode 100644 x/sql_test.go diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index cdbd46427d38..8c001bc87e6e 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -265,7 +265,7 @@ INNER JOIN identity_credentials AND identity_credentials.identity_credential_type_id = ( SELECT id FROM identity_credential_types - WHERE name = ? + WHERE name = ? ) WHERE identity_credentials.config ->> '%s' = ? AND identities.nid = ? @@ -824,14 +824,8 @@ func (p *IdentityPersister) ListIdentities(ctx context.Context, params identity. identifier := params.CredentialsIdentifier identifierOperator := "=" if identifier == "" && params.CredentialsIdentifierSimilar != "" { - identifier = params.CredentialsIdentifierSimilar - identifierOperator = "%" - switch con.Dialect.Name() { - case "postgres", "cockroach": - default: - identifier = "%" + identifier + "%" - identifierOperator = "LIKE" - } + identifier = x.EscapeLikePattern(params.CredentialsIdentifierSimilar) + "%" + identifierOperator = "LIKE" } if len(identifier) > 0 { diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.down.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.down.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.down.sql new file mode 100644 index 000000000000..70d519fb44bc --- /dev/null +++ b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.down.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS btree_gin; + +CREATE INDEX identity_credential_identifiers_nid_identifier_gin ON identity_credential_identifiers USING GIN (nid, identifier gin_trgm_ops); diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.up.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.up.sql new file mode 100644 index 000000000000..159cb60805d1 --- /dev/null +++ b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX identity_credential_identifiers_nid_identifier_gin; diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.up.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/x/sql.go b/x/sql.go new file mode 100644 index 000000000000..3c9a1c181f86 --- /dev/null +++ b/x/sql.go @@ -0,0 +1,10 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import "strings" + +func EscapeLikePattern(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, "\\", "\\\\"), "%", "\\%"), "_", "\\_") +} diff --git a/x/sql_test.go b/x/sql_test.go new file mode 100644 index 000000000000..f8c523dc9e17 --- /dev/null +++ b/x/sql_test.go @@ -0,0 +1,34 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEscapeLikePattern(t *testing.T) { + for name, tc := range map[string]struct { + input string + expected string + }{ + "empty": { + input: "", + expected: "", + }, + "no escape": { + input: "foo", + expected: "foo", + }, + "escape": { + input: "foo%bar_baz\\", + expected: "foo\\%bar\\_baz\\\\", + }, + } { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.expected, EscapeLikePattern(tc.input)) + }) + } +} From fa806aa31350ba8764bb2c16693de555baadc562 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:22:45 +0000 Subject: [PATCH 048/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 34399c2efd57319fd6d711b1df251f769c190683 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:10:16 +0000 Subject: [PATCH 049/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35f647f783bb..0146282e81cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-03-21)](#2024-03-21) +- [ (2024-03-22)](#2024-03-22) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-21) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-22) ## Breaking Changes @@ -347,6 +347,9 @@ defaults to `false`. ([b291c95](https://github.com/ory/kratos/commit/b291c959c18c72f5edc55607ab23b4592faf8d53)) - Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) +- Drop trigram index on identifiers + ([#3827](https://github.com/ory/kratos/issues/3827)) + ([8f8fd90](https://github.com/ory/kratos/commit/8f8fd90304886ecd689a85fc60c4712e47526cdd)) - Ignore decrypt errors in WithDeclassifiedCredentials ([#3731](https://github.com/ory/kratos/issues/3731)) ([8f5192f](https://github.com/ory/kratos/commit/8f5192fbb74c4b952029a6856284de8d59027770)) From d01b6705bf36efb6e0f3d71ed22d0574ab8a98a4 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Fri, 22 Mar 2024 16:36:57 +0100 Subject: [PATCH 050/262] fix: passing transient payloads (#3838) --- internal/client-go/go.sum | 1 + selfservice/strategy/code/hook.jsonnet | 1 + .../strategy/code/strategy_recovery.go | 1 + .../strategy/code/strategy_recovery_test.go | 73 +++++++++++++++++-- selfservice/strategy/oidc/strategy.go | 2 + 5 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 selfservice/strategy/code/hook.jsonnet diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/strategy/code/hook.jsonnet b/selfservice/strategy/code/hook.jsonnet new file mode 100644 index 000000000000..54223dda2f32 --- /dev/null +++ b/selfservice/strategy/code/hook.jsonnet @@ -0,0 +1 @@ +function(ctx) ctx \ No newline at end of file diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 45f9a19e74fc..0cbddf393325 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -386,6 +386,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R return s.HandleRecoveryError(w, r, f, body, err) } + f.TransientPayload = body.TransientPayload if err := s.deps.CodeSender().SendRecoveryCode(ctx, f, identity.VerifiableAddressTypeEmail, body.Email); err != nil { if !errors.Is(err, ErrUnknownAddress) { return s.HandleRecoveryError(w, r, f, body, err) diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go index a5b4cef8bca5..a8bea043f91c 100644 --- a/selfservice/strategy/code/strategy_recovery_test.go +++ b/selfservice/strategy/code/strategy_recovery_test.go @@ -293,9 +293,68 @@ func TestRecovery(t *testing.T) { assert.Contains(t, gjson.Get(body, "redirect_browser_to").String(), "settings-ts?") }) + t.Run("description=should pass transient data to email template and webhooks", func(t *testing.T) { + var webhookReceivedTransientPayload string + webhookTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + webhookReceivedTransientPayload = gjson.GetBytes(ioutilx.MustReadAll(r.Body), "flow.transient_payload").String() + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(webhookTS.Close) + + conf.MustSet( + ctx, + "selfservice.flows.recovery.after.hooks", + []config.SelfServiceHook{{Name: "web_hook", Config: []byte( + fmt.Sprintf(`{ + "method":"POST", + "url": "%s", + "body":"file://./hook.jsonnet" +}`, webhookTS.URL), + )}}, + ) + + t.Cleanup(func() { + conf.MustSet(ctx, "selfservice.flows.recovery.after.hooks", nil) + }) + client := testhelpers.NewClientWithCookies(t) + email := testhelpers.RandomEmail() + createIdentityToRecover(t, reg, email) + templatePayload := `{"payload":"template data"}` + webhookPayload := `{"payload":"webhook data"}` + + f := testhelpers.InitializeRecoveryFlowViaBrowser(t, client, false, public, nil) + + formPayload := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + formPayload.Set("email", email) + formPayload.Set("transient_payload", templatePayload) + + body, _ := testhelpers.RecoveryMakeRequest(t, false, f, client, formPayload.Encode()) + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Recover access to your account") + assert.Equal(t, templatePayload, gjson.GetBytes(message.TemplateData, "transient_payload").String(), + "should pass transient payload to email template") + + recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, recoveryCode) + + action := gjson.Get(body, "ui.action").String() + assert.NotEmpty(t, action) + + _, err := client.Post(action, "application/x-www-form-urlencoded", bytes.NewBufferString( + withCSRFToken(t, RecoveryClientTypeBrowser, body, url.Values{ + "code": {recoveryCode}, + "method": {"code"}, + "transient_payload": {webhookPayload}, + }))) + require.NoError(t, err) + + assert.JSONEq(t, webhookPayload, webhookReceivedTransientPayload, + "should pass transient payload to webhook") + }) + t.Run("description=should return browser to return url", func(t *testing.T) { returnTo := public.URL + "/return-to" - conf.Set(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + for _, tc := range []struct { desc string returnTo string @@ -313,9 +372,9 @@ func TestRecovery(t *testing.T) { desc: "should use return_to from config", returnTo: returnTo, f: func(t *testing.T, client *http.Client, identity *identity.Identity) *kratos.RecoveryFlow { - conf.Set(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, returnTo) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, returnTo) t.Cleanup(func() { - conf.Set(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, "") + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, "") }) return testhelpers.InitializeRecoveryFlowViaBrowser(t, client, false, public, nil) }, @@ -331,10 +390,10 @@ func TestRecovery(t *testing.T) { desc: "should use return_to with an account that has 2fa enabled", returnTo: returnTo, f: func(t *testing.T, client *http.Client, id *identity.Identity) *kratos.RecoveryFlow { - conf.Set(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) - conf.Set(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) - conf.Set(ctx, config.ViperKeyWebAuthnRPDisplayName, "Kratos") - conf.Set(ctx, config.ViperKeyWebAuthnRPID, "ory.sh") + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + conf.MustSet(ctx, config.ViperKeyWebAuthnRPDisplayName, "Kratos") + conf.MustSet(ctx, config.ViperKeyWebAuthnRPID, "ory.sh") t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, identity.AuthenticatorAssuranceLevel1) diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 5283791cd85d..b710d7423c85 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -460,6 +460,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt switch a := req.(type) { case *login.Flow: + a.TransientPayload = cntnr.TransientPayload if ff, err := s.processLogin(w, r, a, et, claims, provider, cntnr); err != nil { if ff != nil { s.forwardError(w, r, ff, err) @@ -479,6 +480,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt } return case *settings.Flow: + a.TransientPayload = cntnr.TransientPayload sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) if err != nil { s.forwardError(w, r, a, s.handleError(w, r, a, pid, nil, err)) From 60537a92c656a8f99fe3368ee1c16877b3bc125a Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:38:34 +0000 Subject: [PATCH 051/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 49d93c0e3383f602fe6be3c7bf749b54f344aa72 Mon Sep 17 00:00:00 2001 From: David Bourgault Date: Sun, 24 Mar 2024 05:35:17 -0400 Subject: [PATCH 052/262] fix: tolerate more "truthy" values when creating new flows (#3841) Use strconv.ParseBool to accept multiple "truthy" values for the `refresh` and `return_session_token_exchange_code` query parameters when creating a new login flow. For some SDKs (e.g.: Python), these stringification of booleans is not user-controlled and these endpoints could not be used fully due to the backend ignoring any value other than `true` (all lowercase). Closes #3839 --- selfservice/flow/login/flow.go | 5 ++++- selfservice/flow/login/flow_test.go | 28 ++++++++++++++++++++++---- selfservice/flow/login/handler.go | 5 ++++- selfservice/flow/login/handler_test.go | 12 +++++++---- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index 9e91deafc678..8b1578e912f5 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -176,6 +177,8 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques return nil, err } + refresh, _ := strconv.ParseBool(r.URL.Query().Get("refresh")) + return &Flow{ ID: id, OAuth2LoginChallenge: hydraLoginChallenge, @@ -188,7 +191,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques RequestURL: requestURL, CSRFToken: csrf, Type: flowType, - Refresh: r.URL.Query().Get("refresh") == "true", + Refresh: refresh, RequestedAAL: identity.AuthenticatorAssuranceLevel(strings.ToLower(stringsx.Coalesce( r.URL.Query().Get("aal"), string(identity.AuthenticatorAssuranceLevel1)))), diff --git a/selfservice/flow/login/flow_test.go b/selfservice/flow/login/flow_test.go index 685d2604c43d..24c9d03dcb52 100644 --- a/selfservice/flow/login/flow_test.go +++ b/selfservice/flow/login/flow_test.go @@ -65,6 +65,24 @@ func TestNewFlow(t *testing.T) { assert.Equal(t, identity.AuthenticatorAssuranceLevel1, r.RequestedAAL) }) + t.Run("type=refresh", func(t *testing.T) { + t.Run("case=refresh accepts any truthy value", func(t *testing.T) { + parameters := []string{"true", "True", "1"} + + for _, refresh := range parameters { + r, err := login.NewFlow(conf, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: fmt.Sprintf("refresh=%v", refresh)}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.True(t, r.Refresh) + } + }) + + t.Run("case=refresh silently ignores invalid values", func(t *testing.T) { + r, err := login.NewFlow(conf, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "refresh=foo"}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.False(t, r.Refresh) + }) + }) + t.Run("type=return_to", func(t *testing.T) { _, err := login.NewFlow(conf, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=https://not-allowed/foobar"}, Host: "ory.sh"}, flow.TypeBrowser) require.Error(t, err) @@ -89,7 +107,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=regular flow creation", func(t *testing.T) { r, err := login.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("https://ory.sh/"), - Host: "ory.sh"}, flow.TypeBrowser) + Host: "ory.sh", + }, flow.TypeBrowser) require.NoError(t, err) assert.Equal(t, "https://ory.sh/", r.RequestURL) }) @@ -99,7 +118,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=flow with refresh", func(t *testing.T) { r, err := login.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("/?refresh=true"), - Host: "ory.sh"}, flow.TypeAPI) + Host: "ory.sh", + }, flow.TypeAPI) require.NoError(t, err) assert.Equal(t, r.IssuedAt, r.ExpiresAt) assert.Equal(t, flow.TypeAPI, r.Type) @@ -110,7 +130,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=flow without refresh", func(t *testing.T) { r, err := login.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("/"), - Host: "ory.sh"}, flow.TypeAPI) + Host: "ory.sh", + }, flow.TypeAPI) require.NoError(t, err) assert.Equal(t, r.IssuedAt, r.ExpiresAt) assert.Equal(t, flow.TypeAPI, r.Type) @@ -129,7 +150,6 @@ func TestNewFlow(t *testing.T) { require.NoError(t, err) assert.Equal(t, "8aadcb8fc1334186a84c4da9813356d9", string(r.OAuth2LoginChallenge)) }) - } func TestFlow(t *testing.T) { diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index b90218ef4fdd..88b3712602a0 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -6,6 +6,7 @@ package login import ( "net/http" "net/url" + "strconv" "time" "github.com/gofrs/uuid" @@ -141,7 +142,9 @@ func (h *Handler) NewLoginFlow(w http.ResponseWriter, r *http.Request, ft flow.T sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r) if e := new(session.ErrNoActiveSessionFound); errors.As(err, &e) { // No session exists yet - if ft == flow.TypeAPI && r.URL.Query().Get("return_session_token_exchange_code") == "true" { + returnSessionTokenExchangeCode, _ := strconv.ParseBool(r.URL.Query().Get("return_session_token_exchange_code")) + + if ft == flow.TypeAPI && returnSessionTokenExchangeCode { e, err := h.d.SessionTokenExchangePersister().CreateSessionTokenExchanger(r.Context(), f.ID) if err != nil { return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err)) diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index 9a82e85ccbc7..813a0c8ad669 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -546,10 +546,14 @@ func TestFlowLifecycle(t *testing.T) { assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) }) - t.Run("case=returns session exchange code", func(t *testing.T) { - res, body := initFlow(t, urlx.ParseOrPanic("/?return_session_token_exchange_code=true").Query(), true) - assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) - assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + t.Run("case=returns session exchange code with any truthy value", func(t *testing.T) { + parameters := []string{"true", "True", "1"} + + for i := range parameters { + res, body := initFlow(t, url.Values{"return_session_token_exchange_code": {parameters[i]}}, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + } }) t.Run("case=can not request refresh and aal at the same time on unauthenticated request", func(t *testing.T) { From 04f02318d4de5290cbf100e9b301284d5ee40fe7 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Sun, 24 Mar 2024 11:16:01 +0100 Subject: [PATCH 053/262] fix(sdk): expand identity in session extension (#3843) Closes #3842 --- session/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session/handler.go b/session/handler.go index 56bd053e551a..3d9d0787404f 100644 --- a/session/handler.go +++ b/session/handler.go @@ -885,7 +885,7 @@ func (h *Handler) adminSessionExtend(w http.ResponseWriter, r *http.Request, ps return } - s, err := h.r.SessionPersister().GetSession(r.Context(), iID, ExpandNothing) + s, err := h.r.SessionPersister().GetSession(r.Context(), iID, ExpandDefault) if err != nil { h.r.Writer().WriteError(w, r, err) return From c08b3ad76c5adb712c945cdbd92a9a51832e94b9 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Sun, 24 Mar 2024 18:06:58 +0100 Subject: [PATCH 054/262] fix: improve SDK discriminators (#3844) --- .schema/openapi/patches/selfservice.yaml | 7 +++- .../client-go/model_update_login_flow_body.go | 40 +++++++++++++++++++ .../model_update_registration_flow_body.go | 40 +++++++++++++++++++ .../model_update_settings_flow_body.go | 40 +++++++++++++++++++ .../model_update_login_flow_body.go | 40 +++++++++++++++++++ .../model_update_registration_flow_body.go | 40 +++++++++++++++++++ .../model_update_settings_flow_body.go | 40 +++++++++++++++++++ spec/api.json | 15 +++++-- 8 files changed, 258 insertions(+), 4 deletions(-) diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index a966cf27401e..81d82247586b 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -18,6 +18,7 @@ - "$ref": "#/components/schemas/updateRegistrationFlowWithOidcMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" - op: add path: /components/schemas/updateRegistrationFlowBody/discriminator value: @@ -27,6 +28,7 @@ oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" code: "#/components/schemas/updateRegistrationFlowWithCodeMethod" + passKey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" - op: add path: /components/schemas/registrationFlowState/enum value: @@ -47,6 +49,7 @@ - "$ref": "#/components/schemas/updateLoginFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" - "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod" - op: add path: /components/schemas/updateLoginFlowBody/discriminator value: @@ -58,6 +61,7 @@ webauthn: "#/components/schemas/updateLoginFlowWithWebAuthnMethod" lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod" code: "#/components/schemas/updateLoginFlowWithCodeMethod" + passkey: "#/components/schemas/updateLoginFlowWithPasskeyMethod" - op: add path: /components/schemas/loginFlowState/enum value: @@ -121,10 +125,10 @@ - "$ref": "#/components/schemas/updateSettingsFlowWithPasswordMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithProfileMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" - - "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithTotpMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithLookupMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithPasskeyMethod" - op: add path: /components/schemas/updateSettingsFlowBody/discriminator value: @@ -135,6 +139,7 @@ oidc: "#/components/schemas/updateSettingsFlowWithOidcMethod" totp: "#/components/schemas/updateSettingsFlowWithTotpMethod" webauthn: "#/components/schemas/updateSettingsFlowWithWebAuthnMethod" + passkey: "#/components/schemas/updateSettingsFlowWithPasskeyMethod" lookup_secret: "#/components/schemas/updateSettingsFlowWithLookupMethod" - op: add path: /components/schemas/settingsFlowState/enum diff --git a/internal/client-go/model_update_login_flow_body.go b/internal/client-go/model_update_login_flow_body.go index ac3e4f503292..b8bb05734e3c 100644 --- a/internal/client-go/model_update_login_flow_body.go +++ b/internal/client-go/model_update_login_flow_body.go @@ -21,6 +21,7 @@ type UpdateLoginFlowBody struct { UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod + UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod @@ -47,6 +48,13 @@ func UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithOi } } +// UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasskeyMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasskeyMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithPasskeyMethod: v, + } +} + // UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasswordMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasswordMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -114,6 +122,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'password' if jsonDict["method"] == "password" { // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod @@ -186,6 +206,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateLoginFlowWithPasskeyMethod' + if jsonDict["method"] == "updateLoginFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateLoginFlowWithPasswordMethod' if jsonDict["method"] == "updateLoginFlowWithPasswordMethod" { // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod @@ -239,6 +271,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateLoginFlowWithOidcMethod) } + if src.UpdateLoginFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasskeyMethod) + } + if src.UpdateLoginFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithPasswordMethod) } @@ -271,6 +307,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { return obj.UpdateLoginFlowWithOidcMethod } + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return obj.UpdateLoginFlowWithPasskeyMethod + } + if obj.UpdateLoginFlowWithPasswordMethod != nil { return obj.UpdateLoginFlowWithPasswordMethod } diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go index 7272ea1ace1a..64374c620f8f 100644 --- a/internal/client-go/model_update_registration_flow_body.go +++ b/internal/client-go/model_update_registration_flow_body.go @@ -20,6 +20,7 @@ import ( type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod + UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -38,6 +39,13 @@ func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateR } } +// UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasskeyMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasskeyMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithPasskeyMethod: v, + } +} + // UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasswordMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasswordMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -86,6 +94,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'passKey' + if jsonDict["method"] == "passKey" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'password' if jsonDict["method"] == "password" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod @@ -134,6 +154,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateRegistrationFlowWithPasskeyMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateRegistrationFlowWithPasswordMethod' if jsonDict["method"] == "updateRegistrationFlowWithPasswordMethod" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod @@ -171,6 +203,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } + if src.UpdateRegistrationFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithPasskeyMethod) + } + if src.UpdateRegistrationFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } @@ -195,6 +231,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithOidcMethod } + if obj.UpdateRegistrationFlowWithPasskeyMethod != nil { + return obj.UpdateRegistrationFlowWithPasskeyMethod + } + if obj.UpdateRegistrationFlowWithPasswordMethod != nil { return obj.UpdateRegistrationFlowWithPasswordMethod } diff --git a/internal/client-go/model_update_settings_flow_body.go b/internal/client-go/model_update_settings_flow_body.go index e2aec380a586..cb1edae41d31 100644 --- a/internal/client-go/model_update_settings_flow_body.go +++ b/internal/client-go/model_update_settings_flow_body.go @@ -20,6 +20,7 @@ import ( type UpdateSettingsFlowBody struct { UpdateSettingsFlowWithLookupMethod *UpdateSettingsFlowWithLookupMethod UpdateSettingsFlowWithOidcMethod *UpdateSettingsFlowWithOidcMethod + UpdateSettingsFlowWithPasskeyMethod *UpdateSettingsFlowWithPasskeyMethod UpdateSettingsFlowWithPasswordMethod *UpdateSettingsFlowWithPasswordMethod UpdateSettingsFlowWithProfileMethod *UpdateSettingsFlowWithProfileMethod UpdateSettingsFlowWithTotpMethod *UpdateSettingsFlowWithTotpMethod @@ -40,6 +41,13 @@ func UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(v *UpdateSettingsF } } +// UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasskeyMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasskeyMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasskeyMethod: v, + } +} + // UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasswordMethod wrapped in UpdateSettingsFlowBody func UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasswordMethod) UpdateSettingsFlowBody { return UpdateSettingsFlowBody{ @@ -102,6 +110,18 @@ func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'password' if jsonDict["method"] == "password" { // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod @@ -174,6 +194,18 @@ func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateSettingsFlowWithPasskeyMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateSettingsFlowWithPasswordMethod' if jsonDict["method"] == "updateSettingsFlowWithPasswordMethod" { // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod @@ -235,6 +267,10 @@ func (src UpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateSettingsFlowWithOidcMethod) } + if src.UpdateSettingsFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasskeyMethod) + } + if src.UpdateSettingsFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateSettingsFlowWithPasswordMethod) } @@ -267,6 +303,10 @@ func (obj *UpdateSettingsFlowBody) GetActualInstance() interface{} { return obj.UpdateSettingsFlowWithOidcMethod } + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return obj.UpdateSettingsFlowWithPasskeyMethod + } + if obj.UpdateSettingsFlowWithPasswordMethod != nil { return obj.UpdateSettingsFlowWithPasswordMethod } diff --git a/internal/httpclient/model_update_login_flow_body.go b/internal/httpclient/model_update_login_flow_body.go index ac3e4f503292..b8bb05734e3c 100644 --- a/internal/httpclient/model_update_login_flow_body.go +++ b/internal/httpclient/model_update_login_flow_body.go @@ -21,6 +21,7 @@ type UpdateLoginFlowBody struct { UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod + UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod @@ -47,6 +48,13 @@ func UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithOi } } +// UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasskeyMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasskeyMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithPasskeyMethod: v, + } +} + // UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasswordMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasswordMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -114,6 +122,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'password' if jsonDict["method"] == "password" { // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod @@ -186,6 +206,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateLoginFlowWithPasskeyMethod' + if jsonDict["method"] == "updateLoginFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateLoginFlowWithPasswordMethod' if jsonDict["method"] == "updateLoginFlowWithPasswordMethod" { // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod @@ -239,6 +271,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateLoginFlowWithOidcMethod) } + if src.UpdateLoginFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasskeyMethod) + } + if src.UpdateLoginFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithPasswordMethod) } @@ -271,6 +307,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { return obj.UpdateLoginFlowWithOidcMethod } + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return obj.UpdateLoginFlowWithPasskeyMethod + } + if obj.UpdateLoginFlowWithPasswordMethod != nil { return obj.UpdateLoginFlowWithPasswordMethod } diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go index 7272ea1ace1a..64374c620f8f 100644 --- a/internal/httpclient/model_update_registration_flow_body.go +++ b/internal/httpclient/model_update_registration_flow_body.go @@ -20,6 +20,7 @@ import ( type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod + UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -38,6 +39,13 @@ func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateR } } +// UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasskeyMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasskeyMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithPasskeyMethod: v, + } +} + // UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasswordMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasswordMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -86,6 +94,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'passKey' + if jsonDict["method"] == "passKey" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'password' if jsonDict["method"] == "password" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod @@ -134,6 +154,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateRegistrationFlowWithPasskeyMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateRegistrationFlowWithPasswordMethod' if jsonDict["method"] == "updateRegistrationFlowWithPasswordMethod" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod @@ -171,6 +203,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } + if src.UpdateRegistrationFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithPasskeyMethod) + } + if src.UpdateRegistrationFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } @@ -195,6 +231,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithOidcMethod } + if obj.UpdateRegistrationFlowWithPasskeyMethod != nil { + return obj.UpdateRegistrationFlowWithPasskeyMethod + } + if obj.UpdateRegistrationFlowWithPasswordMethod != nil { return obj.UpdateRegistrationFlowWithPasswordMethod } diff --git a/internal/httpclient/model_update_settings_flow_body.go b/internal/httpclient/model_update_settings_flow_body.go index e2aec380a586..cb1edae41d31 100644 --- a/internal/httpclient/model_update_settings_flow_body.go +++ b/internal/httpclient/model_update_settings_flow_body.go @@ -20,6 +20,7 @@ import ( type UpdateSettingsFlowBody struct { UpdateSettingsFlowWithLookupMethod *UpdateSettingsFlowWithLookupMethod UpdateSettingsFlowWithOidcMethod *UpdateSettingsFlowWithOidcMethod + UpdateSettingsFlowWithPasskeyMethod *UpdateSettingsFlowWithPasskeyMethod UpdateSettingsFlowWithPasswordMethod *UpdateSettingsFlowWithPasswordMethod UpdateSettingsFlowWithProfileMethod *UpdateSettingsFlowWithProfileMethod UpdateSettingsFlowWithTotpMethod *UpdateSettingsFlowWithTotpMethod @@ -40,6 +41,13 @@ func UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(v *UpdateSettingsF } } +// UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasskeyMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasskeyMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasskeyMethod: v, + } +} + // UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasswordMethod wrapped in UpdateSettingsFlowBody func UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasswordMethod) UpdateSettingsFlowBody { return UpdateSettingsFlowBody{ @@ -102,6 +110,18 @@ func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'password' if jsonDict["method"] == "password" { // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod @@ -174,6 +194,18 @@ func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateSettingsFlowWithPasskeyMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateSettingsFlowWithPasswordMethod' if jsonDict["method"] == "updateSettingsFlowWithPasswordMethod" { // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod @@ -235,6 +267,10 @@ func (src UpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateSettingsFlowWithOidcMethod) } + if src.UpdateSettingsFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasskeyMethod) + } + if src.UpdateSettingsFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateSettingsFlowWithPasswordMethod) } @@ -267,6 +303,10 @@ func (obj *UpdateSettingsFlowBody) GetActualInstance() interface{} { return obj.UpdateSettingsFlowWithOidcMethod } + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return obj.UpdateSettingsFlowWithPasskeyMethod + } + if obj.UpdateSettingsFlowWithPasswordMethod != nil { return obj.UpdateSettingsFlowWithPasswordMethod } diff --git a/spec/api.json b/spec/api.json index 0f46469df162..84202310f0a4 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2594,6 +2594,7 @@ "code": "#/components/schemas/updateLoginFlowWithCodeMethod", "lookup_secret": "#/components/schemas/updateLoginFlowWithLookupSecretMethod", "oidc": "#/components/schemas/updateLoginFlowWithOidcMethod", + "passkey": "#/components/schemas/updateLoginFlowWithPasskeyMethod", "password": "#/components/schemas/updateLoginFlowWithPasswordMethod", "totp": "#/components/schemas/updateLoginFlowWithTotpMethod", "webauthn": "#/components/schemas/updateLoginFlowWithWebAuthnMethod" @@ -2618,6 +2619,9 @@ }, { "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" + }, + { + "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod" } ] }, @@ -2920,6 +2924,7 @@ "mapping": { "code": "#/components/schemas/updateRegistrationFlowWithCodeMethod", "oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod", + "passKey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod", "password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod", "webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" }, @@ -2937,6 +2942,9 @@ }, { "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" + }, + { + "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" } ] }, @@ -3147,6 +3155,7 @@ "mapping": { "lookup_secret": "#/components/schemas/updateSettingsFlowWithLookupMethod", "oidc": "#/components/schemas/updateSettingsFlowWithOidcMethod", + "passkey": "#/components/schemas/updateSettingsFlowWithPasskeyMethod", "password": "#/components/schemas/updateSettingsFlowWithPasswordMethod", "profile": "#/components/schemas/updateSettingsFlowWithProfileMethod", "totp": "#/components/schemas/updateSettingsFlowWithTotpMethod", @@ -3164,9 +3173,6 @@ { "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" }, - { - "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" - }, { "$ref": "#/components/schemas/updateSettingsFlowWithTotpMethod" }, @@ -3175,6 +3181,9 @@ }, { "$ref": "#/components/schemas/updateSettingsFlowWithLookupMethod" + }, + { + "$ref": "#/components/schemas/updateSettingsFlowWithPasskeyMethod" } ] }, From 0713e2dcbedafd0a1134063186ce11e44ba709bf Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:42:21 +0100 Subject: [PATCH 055/262] chore: upgrade ory/x to v0.0.619 (#3845) --- go.mod | 3 ++- go.sum | 4 ++++ internal/client-go/go.sum | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 14593dd5874f..9d0ec28c0544 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.616 + github.com/ory/x v0.0.619 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 @@ -335,6 +335,7 @@ require ( ) require ( + github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/jackc/puddle/v2 v2.1.2 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/segmentio/asm v1.2.0 // indirect diff --git a/go.sum b/go.sum index 9db641152989..fc067a3bd5e3 100644 --- a/go.sum +++ b/go.sum @@ -199,6 +199,8 @@ github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= +github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-crypt/crypt v0.2.9 h1:5gWWTId2Qyqs9ROIsegt5pnqo9wUSRLbhpkR6JSftjg= github.com/go-crypt/crypt v0.2.9/go.mod h1:JjzdTYE2mArb6nBoIvvpF7o46/rK/1pfmlArCRMTFUk= github.com/go-crypt/x v0.2.1 h1:OGw78Bswme9lffCOX6tyuC280ouU5391glsvThMtM5U= @@ -824,6 +826,8 @@ github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpi github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/ory/x v0.0.616 h1:iaojp7MvFW1cdirSZFK/XeuJvyhUEVXQdY61bmIOkzk= github.com/ory/x v0.0.616/go.mod h1:Fqxxc1Ks6a4vZuqWwr6TYAeUDh2SAvxXyrk9N7Hidbo= +github.com/ory/x v0.0.619 h1:eeK6BVeko0MBg7HERrPOu1B0YRUOu/wLBkWqEdHxYF0= +github.com/ory/x v0.0.619/go.mod h1:Fqxxc1Ks6a4vZuqWwr6TYAeUDh2SAvxXyrk9N7Hidbo= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 5aad1c1e6cc92f72af56511dacb9812edb600813 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 26 Mar 2024 11:11:22 +0100 Subject: [PATCH 056/262] fix: execute verification & verification_ui properly in login flows (#3847) --- go.mod | 15 +- go.sum | 27 ++-- internal/client-go/go.sum | 1 - selfservice/flow/login/flow.go | 7 + selfservice/flow/login/hook.go | 3 + selfservice/flow/registration/flow.go | 4 + selfservice/hook/show_verification_ui.go | 26 ++- selfservice/hook/show_verification_ui_test.go | 152 ++++++++++++------ selfservice/hook/verification.go | 2 +- spec/api.json | 2 + spec/swagger.json | 2 + 11 files changed, 167 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index 9d0ec28c0544..655a07bbf9e2 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.619 + github.com/ory/x v0.0.623 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 @@ -207,13 +207,13 @@ require ( github.com/ian-kent/linkio v0.0.0-20170807205755-97566b872887 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.13.0 // indirect + github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.1 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.12.0 // indirect - github.com/jackc/pgx/v4 v4.17.2 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/pgx/v4 v4.18.2 // indirect github.com/jandelgado/gcov2lcov v1.0.5 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect github.com/jinzhu/copier v0.3.5 // indirect @@ -316,7 +316,7 @@ require ( google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -335,7 +335,6 @@ require ( ) require ( - github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/jackc/puddle/v2 v2.1.2 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/segmentio/asm v1.2.0 // indirect diff --git a/go.sum b/go.sum index fc067a3bd5e3..f344856176b4 100644 --- a/go.sum +++ b/go.sum @@ -199,8 +199,6 @@ github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE= -github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-crypt/crypt v0.2.9 h1:5gWWTId2Qyqs9ROIsegt5pnqo9wUSRLbhpkR6JSftjg= github.com/go-crypt/crypt v0.2.9/go.mod h1:JjzdTYE2mArb6nBoIvvpF7o46/rK/1pfmlArCRMTFUk= github.com/go-crypt/x v0.2.1 h1:OGw78Bswme9lffCOX6tyuC280ouU5391glsvThMtM5U= @@ -555,8 +553,9 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -572,22 +571,26 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -824,10 +827,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.616 h1:iaojp7MvFW1cdirSZFK/XeuJvyhUEVXQdY61bmIOkzk= -github.com/ory/x v0.0.616/go.mod h1:Fqxxc1Ks6a4vZuqWwr6TYAeUDh2SAvxXyrk9N7Hidbo= -github.com/ory/x v0.0.619 h1:eeK6BVeko0MBg7HERrPOu1B0YRUOu/wLBkWqEdHxYF0= -github.com/ory/x v0.0.619/go.mod h1:Fqxxc1Ks6a4vZuqWwr6TYAeUDh2SAvxXyrk9N7Hidbo= +github.com/ory/x v0.0.623 h1:sFJiw2i/itTkBRJbhGXtrso9NcdscnjFlHBFitCzf8A= +github.com/ory/x v0.0.623/go.mod h1:CUw8/O3X8lUMheyV0iH+6LQ0tePrH+FBsW39MccCHgw= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1545,8 +1546,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index 8b1578e912f5..a01d449a2751 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -152,6 +152,9 @@ type Flow struct { // It can, for example, contain a reference to the verification flow, created as part of the user's // registration. ContinueWithItems []flow.ContinueWith `json:"-" db:"-" faker:"-" ` + + // ReturnToVerification contains the redirect URL for the verification flow. + ReturnToVerification string `json:"-" db:"-"` } var _ flow.Flow = new(Flow) @@ -320,3 +323,7 @@ func (f *Flow) AddContinueWith(c flow.ContinueWith) { func (f *Flow) ContinueWith() []flow.ContinueWith { return f.ContinueWithItems } + +func (f *Flow) SetReturnToVerification(to string) { + f.ReturnToVerification = to +} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 595fdbff7936..f0e06ccfc934 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -320,6 +320,9 @@ func (e *HookExecutor) PostLoginHook( } finalReturnTo = rt span.SetAttributes(attribute.String("return_to", rt), attribute.String("redirect_reason", "oauth2 login challenge")) + } else if f.ReturnToVerification != "" { + finalReturnTo = f.ReturnToVerification + span.SetAttributes(attribute.String("redirect_reason", "verification requested")) } x.ContentNegotiationRedirection(w, r, s, e.d.Writer(), finalReturnTo) diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index 2786a03fdc79..17ad0c6d720a 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -275,3 +275,7 @@ func (f *Flow) SetState(state State) { func (t *Flow) GetTransientPayload() json.RawMessage { return t.TransientPayload } + +func (f *Flow) SetReturnToVerification(to string) { + f.ReturnToVerification = to +} diff --git a/selfservice/hook/show_verification_ui.go b/selfservice/hook/show_verification_ui.go index 51f29a2aa6ce..566546a028da 100644 --- a/selfservice/hook/show_verification_ui.go +++ b/selfservice/hook/show_verification_ui.go @@ -9,13 +9,18 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/otelx" ) -var _ registration.PostHookPostPersistExecutor = new(ShowVerificationUIHook) +var ( + _ registration.PostHookPostPersistExecutor = new(ShowVerificationUIHook) + _ login.PostHookExecutor = new(ShowVerificationUIHook) +) type ( showVerificationUIDependencies interface { @@ -45,7 +50,20 @@ func (e *ShowVerificationUIHook) ExecutePostRegistrationPostPersistHook(_ http.R }) } -func (e *ShowVerificationUIHook) execute(r *http.Request, f *registration.Flow) error { +// ExecutePostRegistrationPostPersistHook adds redirect headers and status code if the request is a browser request. +// If the request is not a browser request, this hook does nothing. +func (e *ShowVerificationUIHook) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, _ *session.Session) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.ShowVerificationUIHook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { + return e.execute(r.WithContext(ctx), f) + }) +} + +type loginOrRegistrationFlow interface { + ContinueWith() []flow.ContinueWith + SetReturnToVerification(string) +} + +func (e *ShowVerificationUIHook) execute(r *http.Request, f loginOrRegistrationFlow) error { if !x.IsBrowserRequest(r) { // this hook is only intended to be used by browsers, as it redirects to the verification ui // JSON API clients should use the `continue_with` field to continue the flow @@ -53,7 +71,7 @@ func (e *ShowVerificationUIHook) execute(r *http.Request, f *registration.Flow) } var vf *flow.ContinueWithVerificationUI - for _, c := range f.ContinueWithItems { + for _, c := range f.ContinueWith() { if item, ok := c.(*flow.ContinueWithVerificationUI); ok { vf = item } @@ -62,7 +80,7 @@ func (e *ShowVerificationUIHook) execute(r *http.Request, f *registration.Flow) ctx := r.Context() if vf != nil { redirURL := e.d.Config().SelfServiceFlowVerificationUI(ctx) - f.ReturnToVerification = vf.AppendTo(redirURL).String() + f.SetReturnToVerification(vf.AppendTo(redirURL).String()) } return nil diff --git a/selfservice/hook/show_verification_ui_test.go b/selfservice/hook/show_verification_ui_test.go index 70e14af11711..22171f0c345b 100644 --- a/selfservice/hook/show_verification_ui_test.go +++ b/selfservice/hook/show_verification_ui_test.go @@ -15,62 +15,120 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" ) func TestExecutePostRegistrationPostPersistHook(t *testing.T) { - t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { - _, reg := internal.NewVeryFastRegistryWithoutDB(t) - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - f := ®istration.Flow{} - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil)) - require.Equal(t, 200, rec.Code) - }) + t.Run("flow=registration", func(t *testing.T) { + t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + f := ®istration.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil)) + require.Equal(t, 200, rec.Code) + }) - t.Run("case=not a browser request returns 200 OK", func(t *testing.T) { - _, reg := internal.NewVeryFastRegistryWithoutDB(t) - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - browserRequest.Header.Add("Accept", "application/json") - f := ®istration.Flow{} - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil)) - require.Equal(t, 200, rec.Code) - }) + t.Run("case=not a browser request returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + browserRequest.Header.Add("Accept", "application/json") + f := ®istration.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil)) + require.Equal(t, 200, rec.Code) + }) + + t.Run("case=verification ui in continue with item returns redirect", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + vf := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + } + rf := ®istration.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithVerificationUI(vf, "some@ory.sh", ""), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil)) + assert.Equal(t, 200, rec.Code) + assert.Equal(t, "/verification?flow="+vf.ID.String(), rf.ReturnToVerification) + }) - t.Run("case=verification ui in continue with item returns redirect", func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - vf := &verification.Flow{ - ID: uuid.Must(uuid.NewV4()), - } - rf := ®istration.Flow{} - rf.ContinueWithItems = []flow.ContinueWith{ - flow.NewContinueWithVerificationUI(vf, "some@ory.sh", ""), - } - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil)) - assert.Equal(t, 200, rec.Code) - assert.Equal(t, "/verification?flow="+vf.ID.String(), rf.ReturnToVerification) + t.Run("case=no verification ui in continue with item returns 200 OK", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + rf := ®istration.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithSetToken("token"), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil)) + assert.Equal(t, 200, rec.Code) + }) }) - t.Run("case=no verification ui in continue with item returns 200 OK", func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - rf := ®istration.Flow{} - rf.ContinueWithItems = []flow.ContinueWith{ - flow.NewContinueWithSetToken("token"), - } - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil)) - assert.Equal(t, 200, rec.Code) + t.Run("flow=login", func(t *testing.T) { + t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + f := &login.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil)) + require.Equal(t, 200, rec.Code) + }) + + t.Run("case=not a browser request returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + browserRequest.Header.Add("Accept", "application/json") + f := &login.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil)) + require.Equal(t, 200, rec.Code) + }) + + t.Run("case=verification ui in continue with item returns redirect", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + vf := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + } + rf := &login.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithVerificationUI(vf, "some@ory.sh", ""), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil)) + assert.Equal(t, 200, rec.Code) + assert.Equal(t, "/verification?flow="+vf.ID.String(), rf.ReturnToVerification) + }) + + t.Run("case=no verification ui in continue with item returns 200 OK", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + rf := &login.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithSetToken("token"), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil)) + assert.Equal(t, 200, rec.Code) + }) }) } diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index ef4bf2e3f16a..6fdd039c146f 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -94,7 +94,7 @@ func (e *Verifier) do( } isBrowserFlow := f.GetType() == flow.TypeBrowser - isRegistrationOrLoginFlow := f.GetFlowName() == flow.RegistrationFlow + isRegistrationOrLoginFlow := f.GetFlowName() == flow.RegistrationFlow || f.GetFlowName() == flow.LoginFlow for k := range i.VerifiableAddresses { address := &i.VerifiableAddresses[k] diff --git a/spec/api.json b/spec/api.json index 84202310f0a4..76036e26595f 100644 --- a/spec/api.json +++ b/spec/api.json @@ -879,6 +879,7 @@ "type": "object" } }, + "title": "The not ready status of the service.", "type": "object" }, "healthStatus": { @@ -888,6 +889,7 @@ "type": "string" } }, + "title": "The health status of the service.", "type": "object" }, "identity": { diff --git a/spec/swagger.json b/spec/swagger.json index 37a79fc4f6bb..7da943e9f0a8 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3991,6 +3991,7 @@ }, "healthNotReadyStatus": { "type": "object", + "title": "The not ready status of the service.", "properties": { "errors": { "description": "Errors contains a list of errors that caused the not ready status.", @@ -4003,6 +4004,7 @@ }, "healthStatus": { "type": "object", + "title": "The health status of the service.", "properties": { "status": { "description": "Status always contains \"ok\".", From b7fd23b217b41406e0eea54834d604bc7cfb76ca Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:03:54 +0000 Subject: [PATCH 057/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0146282e81cb..6d5dabc7b428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-03-22)](#2024-03-22) +- [ (2024-03-26)](#2024-03-26) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-22) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-26) ## Breaking Changes @@ -350,18 +350,31 @@ defaults to `false`. - Drop trigram index on identifiers ([#3827](https://github.com/ory/kratos/issues/3827)) ([8f8fd90](https://github.com/ory/kratos/commit/8f8fd90304886ecd689a85fc60c4712e47526cdd)) +- Execute verification & verification_ui properly in login flows + ([#3847](https://github.com/ory/kratos/issues/3847)) + ([5aad1c1](https://github.com/ory/kratos/commit/5aad1c1e6cc92f72af56511dacb9812edb600813)) - Ignore decrypt errors in WithDeclassifiedCredentials ([#3731](https://github.com/ory/kratos/issues/3731)) ([8f5192f](https://github.com/ory/kratos/commit/8f5192fbb74c4b952029a6856284de8d59027770)) +- Improve SDK discriminators + ([#3844](https://github.com/ory/kratos/issues/3844)) + ([c08b3ad](https://github.com/ory/kratos/commit/c08b3ad76c5adb712c945cdbd92a9a51832e94b9)) - Make sure emails can still be sent with SMS enabled ([#3795](https://github.com/ory/kratos/issues/3795)) ([7c68c5a](https://github.com/ory/kratos/commit/7c68c5aa69ed76a84a37a37a3555277ddc772cf8)) - Missing indices and foreign keys ([#3800](https://github.com/ory/kratos/issues/3800)) ([0b32ce1](https://github.com/ory/kratos/commit/0b32ce113be47aa724d3468062ced09f8f60c52a)) +- Passing transient payloads + ([#3838](https://github.com/ory/kratos/issues/3838)) + ([d01b670](https://github.com/ory/kratos/commit/d01b6705bf36efb6e0f3d71ed22d0574ab8a98a4)) - Prevent SMTP URL leak on unparsable URL ([#3770](https://github.com/ory/kratos/issues/3770)) ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) +- **sdk:** Expand identity in session extension + ([#3843](https://github.com/ory/kratos/issues/3843)) + ([04f0231](https://github.com/ory/kratos/commit/04f02318d4de5290cbf100e9b301284d5ee40fe7)), + closes [#3842](https://github.com/ory/kratos/issues/3842) - **sdk:** Improve discriminators for node and Go ([#3821](https://github.com/ory/kratos/issues/3821)) ([9ddf7cc](https://github.com/ory/kratos/commit/9ddf7cc7c52313c4ee13ccdc2886ad94b5d1317f)) @@ -371,6 +384,18 @@ defaults to `false`. - Test assertions on declassifying OIDC tokens ([#3773](https://github.com/ory/kratos/issues/3773)) ([7f8a7f1](https://github.com/ory/kratos/commit/7f8a7f142a91c8c74f32eadb41224fc4f69c2109)) +- Tolerate more "truthy" values when creating new flows + ([#3841](https://github.com/ory/kratos/issues/3841)) + ([49d93c0](https://github.com/ory/kratos/commit/49d93c0e3383f602fe6be3c7bf749b54f344aa72)), + closes [#3839](https://github.com/ory/kratos/issues/3839): + + Use strconv.ParseBool to accept multiple "truthy" values for the `refresh` and + `return_session_token_exchange_code` query parameters when creating a new + login flow. + + For some SDKs (e.g.: Python), these stringification of booleans is not + user-controlled and these endpoints could not be used fully due to the backend + ignoring any value other than `true` (all lowercase). ### Features From 8eee972d89accb02b3caa053fca2f16ed2c876f1 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Tue, 26 Mar 2024 14:22:18 +0100 Subject: [PATCH 058/262] fix: don't treat passkeys as AAL2 (#3853) --- selfservice/strategy/passkey/passkey_registration_test.go | 1 + selfservice/strategy/passkey/passkey_strategy.go | 2 +- test/e2e/profiles/passkey/.kratos.yml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go index a6ab50b29b61..1a4759dfa09e 100644 --- a/selfservice/strategy/passkey/passkey_registration_test.go +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -297,6 +297,7 @@ func TestRegistration(t *testing.T) { i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) require.NoError(t, err) + assert.Equal(t, "aal1", i.AvailableAAL.String) assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) }) } diff --git a/selfservice/strategy/passkey/passkey_strategy.go b/selfservice/strategy/passkey/passkey_strategy.go index dbf550d352ff..b590a7e93b6d 100644 --- a/selfservice/strategy/passkey/passkey_strategy.go +++ b/selfservice/strategy/passkey/passkey_strategy.go @@ -96,7 +96,7 @@ func (s *Strategy) CompletedAuthenticationMethod(context.Context, session.Authen } func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { - return s.countCredentials(cc) + return 0, nil } func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { diff --git a/test/e2e/profiles/passkey/.kratos.yml b/test/e2e/profiles/passkey/.kratos.yml index cbe13e07ef1e..85441f599e1b 100644 --- a/test/e2e/profiles/passkey/.kratos.yml +++ b/test/e2e/profiles/passkey/.kratos.yml @@ -3,7 +3,7 @@ selfservice: settings: ui_url: http://localhost:4455/settings privileged_session_max_age: 5m - required_aal: aal1 + required_aal: highest_available logout: after: @@ -52,4 +52,4 @@ identity: session: whoami: - required_aal: aal1 + required_aal: highest_available From ad0619d803cd2842a67c56a545ec5ab252501b0f Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:41:17 +0100 Subject: [PATCH 059/262] fix: drop index if exists (#3846) --- ...325153839000000_drop_identity_search_index.cockroach.down.sql | 1 + ...40325153839000000_drop_identity_search_index.cockroach.up.sql | 1 + .../sql/20240325153839000000_drop_identity_search_index.down.sql | 0 .../sql/20240325153839000000_drop_identity_search_index.up.sql | 0 4 files changed, 2 insertions(+) create mode 100644 persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.down.sql create mode 100644 persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.up.sql create mode 100644 persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.down.sql create mode 100644 persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.up.sql diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.down.sql new file mode 100644 index 000000000000..f692e9943649 --- /dev/null +++ b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.down.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_identifier_gin ON identity_credential_identifiers USING GIN (nid, identifier gin_trgm_ops); diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.up.sql new file mode 100644 index 000000000000..d1c8cdf26841 --- /dev/null +++ b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS identity_credential_identifiers_nid_identifier_gin; diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.down.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.up.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 From da90502dc3bf8e3d34fb4ecc531834b1919989ad Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 26 Mar 2024 18:24:00 +0100 Subject: [PATCH 060/262] fix: add missing env vars to set up guide (#3855) Closes https://github.com/ory/kratos/issues/3828 --- quickstart.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quickstart.yml b/quickstart.yml index d674f51a4458..68ebaa60185b 100644 --- a/quickstart.yml +++ b/quickstart.yml @@ -21,6 +21,9 @@ services: environment: - KRATOS_PUBLIC_URL=http://kratos:4433/ - KRATOS_BROWSER_URL=http://127.0.0.1:4433/ + - COOKIE_SECRET=changeme + - CSRF_COOKIE_NAME=ory_csrf_ui + - CSRF_COOKIE_SECRET=changeme networks: - intranet restart: on-failure From 4642de0cfd1fb15bc48c7093be9449abd488755c Mon Sep 17 00:00:00 2001 From: Oleksandra Talalaieva <25621530+sashatalalasha@users.noreply.github.com> Date: Wed, 27 Mar 2024 11:32:50 +0100 Subject: [PATCH 061/262] feat: add headers to web hooks (#3849) --- driver/config/config_test.go | 26 ++--- .../config/stub/.kratos.webauthn.invalid.yaml | 26 +++++ .../config/stub/.kratos.webauthn.origin.yaml | 26 +++++ .../config/stub/.kratos.webauthn.origins.yaml | 26 +++++ driver/config/stub/.kratos.yaml | 26 +++++ driver/registry_default_test.go | 110 +++++++++--------- embedx/config.schema.json | 7 ++ 7 files changed, 179 insertions(+), 68 deletions(-) diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 2a8d57e7bebb..6cb37f100850 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -227,7 +227,7 @@ func TestViperProvider(t *testing.T) { t.Run("hook=before", func(t *testing.T) { expHooks := []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"method":"GET","url":"https://test.kratos.ory.sh/before_registration_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/before_registration_hook"}`)}, {Name: "two_step_registration", Config: json.RawMessage(`{}`)}, } @@ -246,7 +246,7 @@ func TestViperProvider(t *testing.T) { strategy: "password", hooks: []config.SelfServiceHook{ {Name: "session", Config: json.RawMessage(`{}`)}, - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_registration_password_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_registration_password_hook"}`)}, // {Name: "verify", Config: json.RawMessage(`{}`)}, // {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)}, }, @@ -255,7 +255,7 @@ func TestViperProvider(t *testing.T) { strategy: "oidc", hooks: []config.SelfServiceHook{ // {Name: "verify", Config: json.RawMessage(`{}`)}, - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_registration_oidc_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_registration_oidc_hook"}`)}, {Name: "session", Config: json.RawMessage(`{}`)}, // {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)}, }, @@ -263,7 +263,7 @@ func TestViperProvider(t *testing.T) { { strategy: config.HookGlobal, hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"in":"header","name":"My-Key","value":"My-Key-Value"},"type":"api_key"},"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_registration_global_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"in":"header","name":"My-Key","value":"My-Key-Value"},"type":"api_key"},"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_registration_global_hook"}`)}, }, }, } { @@ -283,7 +283,7 @@ func TestViperProvider(t *testing.T) { t.Run("hook=before", func(t *testing.T) { expHooks := []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"method":"POST","url":"https://test.kratos.ory.sh/before_login_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/before_login_hook"}`)}, } hooks := p.SelfServiceFlowLoginBeforeHooks(ctx) @@ -303,20 +303,20 @@ func TestViperProvider(t *testing.T) { hooks: []config.SelfServiceHook{ {Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)}, {Name: "require_verified_address", Config: json.RawMessage(`{}`)}, - {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"password":"super-secret","user":"test-user"},"type":"basic_auth"},"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_login_password_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"password":"super-secret","user":"test-user"},"type":"basic_auth"},"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_login_password_hook"}`)}, }, }, { strategy: "oidc", hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_login_oidc_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_login_oidc_hook"}`)}, {Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)}, }, }, { strategy: config.HookGlobal, hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_login_global_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_login_global_hook"}`)}, }, }, } { @@ -338,19 +338,19 @@ func TestViperProvider(t *testing.T) { { strategy: "password", hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_settings_password_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_password_hook"}`)}, }, }, { strategy: "profile", hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_settings_profile_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_profile_hook"}`)}, }, }, { strategy: config.HookGlobal, hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_settings_global_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_global_hook"}`)}, }, }, } { @@ -367,7 +367,7 @@ func TestViperProvider(t *testing.T) { assert.Equal(t, "http://test.kratos.ory.sh/recovery", p.SelfServiceFlowRecoveryUI(ctx).String()) hooks := p.SelfServiceFlowRecoveryAfterHooks(ctx, config.HookGlobal) - assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_recovery_hook"}`)}}, hooks) + assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_recovery_hook"}`)}}, hooks) }) t.Run("method=verification", func(t *testing.T) { @@ -375,7 +375,7 @@ func TestViperProvider(t *testing.T) { assert.Equal(t, "http://test.kratos.ory.sh/verification", p.SelfServiceFlowVerificationUI(ctx).String()) hooks := p.SelfServiceFlowVerificationAfterHooks(ctx, config.HookGlobal) - assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_verification_hook"}`)}}, hooks) + assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_verification_hook"}`)}}, hooks) }) t.Run("group=hashers", func(t *testing.T) { diff --git a/driver/config/stub/.kratos.webauthn.invalid.yaml b/driver/config/stub/.kratos.webauthn.invalid.yaml index 01fc9f5adaad..a1230588c666 100644 --- a/driver/config/stub/.kratos.webauthn.invalid.yaml +++ b/driver/config/stub/.kratos.webauthn.invalid.yaml @@ -104,6 +104,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -117,6 +119,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -132,6 +136,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -139,12 +145,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -156,6 +166,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -167,6 +179,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -179,6 +193,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -186,6 +202,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -198,6 +216,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -207,12 +227,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -227,5 +251,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.webauthn.origin.yaml b/driver/config/stub/.kratos.webauthn.origin.yaml index 1b2ff11e9d8f..95deec00910d 100644 --- a/driver/config/stub/.kratos.webauthn.origin.yaml +++ b/driver/config/stub/.kratos.webauthn.origin.yaml @@ -100,6 +100,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -113,6 +115,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -128,6 +132,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -135,12 +141,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -152,6 +162,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -163,6 +175,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -175,6 +189,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -182,6 +198,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -194,6 +212,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -203,12 +223,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -223,5 +247,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.webauthn.origins.yaml b/driver/config/stub/.kratos.webauthn.origins.yaml index f9349c670ac0..9efeb34e0b02 100644 --- a/driver/config/stub/.kratos.webauthn.origins.yaml +++ b/driver/config/stub/.kratos.webauthn.origins.yaml @@ -103,6 +103,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -116,6 +118,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -131,6 +135,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -138,12 +144,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -155,6 +165,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -166,6 +178,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -178,6 +192,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -185,6 +201,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -197,6 +215,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -206,12 +226,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -226,5 +250,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.yaml b/driver/config/stub/.kratos.yaml index b50341928ad8..bc35439f8a53 100644 --- a/driver/config/stub/.kratos.yaml +++ b/driver/config/stub/.kratos.yaml @@ -99,6 +99,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -112,6 +114,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -127,6 +131,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -134,12 +140,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -151,6 +161,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -162,6 +174,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -174,6 +188,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -181,6 +197,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -193,6 +211,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -202,12 +222,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -222,5 +246,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index d1c02490dcb5..009dd76173d8 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -51,14 +51,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceVerificationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return []verification.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -90,14 +90,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Multiple web_hooks configured", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor { return []verification.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -132,14 +132,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return []recovery.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -171,14 +171,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Multiple web_hooks configured", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return []recovery.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -217,14 +217,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), hook.NewTwoStepRegistration(reg), } }, @@ -273,14 +273,14 @@ func TestDriverDefault_Hooks(t *testing.T) { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "body": "bar"}}, + {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "session"}, }) }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), hook.NewSessionIssuer(reg), } }, @@ -289,14 +289,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured on a global level", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -304,18 +304,18 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Hooks are configured on a global level, as well as on a strategy level", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "session"}, }) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "POST"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"foo"}`)), hook.NewSessionIssuer(reg), } }, @@ -364,14 +364,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return []login.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -429,14 +429,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "A revoke_active_sessions hook, require_verified_address hook and a web_hook are configured for password strategy", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "body": "bar"}}, + {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "require_verified_address"}, {"hook": "revoke_active_sessions"}, }) }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), hook.NewAddressVerifier(), hook.NewSessionDestroyer(reg), } @@ -446,14 +446,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured on a global level", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -461,17 +461,17 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Hooks are configured on a global level, as well as on a strategy level", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "revoke_active_sessions"}, {"hook": "require_verified_address"}, }) conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"foo"}`)), hook.NewSessionDestroyer(reg), hook.NewAddressVerifier(), } @@ -508,14 +508,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return []settings.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -561,14 +561,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "A verify hook and a web_hook are configured for profile strategy", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "body": "bar"}}, + {"hook": "web_hook", "config": map[string]interface{}{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}}, }) conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","headers":[{"X-Custom-Header":"test"}],"method":"POST","url":"foo"}`)), } }, }, @@ -576,14 +576,14 @@ func TestDriverDefault_Hooks(t *testing.T) { uc: "Two web_hooks are configured on a global level", prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, @@ -592,16 +592,16 @@ func TestDriverDefault_Hooks(t *testing.T) { prep: func(conf *config.Config) { conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, + {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, }) }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"foo"}`)), } }, }, diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 15cc950fbf8e..0b540ea25cd6 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -263,6 +263,13 @@ "type": "string", "description": "The HTTP method to use (GET, POST, etc)." }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the Web-Hook", + "additionalProperties": { + "type": "string" + } + }, "body": { "type": "string", "oneOf": [ From 2cdfc70c726a166790b98d419895f0396d13176f Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Thu, 28 Mar 2024 14:03:52 +0100 Subject: [PATCH 062/262] fix: webhook transient payload in OIDC login flows (#3857) * fix: transient payload with OIDC login --- internal/client-go/go.sum | 1 + .../hook/hooktest/web_hook_test_server.go | 74 +++++++++++++++++++ selfservice/strategy/code/hook.jsonnet | 1 - .../strategy/code/strategy_recovery_test.go | 27 ++----- selfservice/strategy/oidc/strategy_login.go | 7 +- selfservice/strategy/oidc/strategy_test.go | 45 ++++++++++- 6 files changed, 127 insertions(+), 28 deletions(-) create mode 100644 selfservice/hook/hooktest/web_hook_test_server.go delete mode 100644 selfservice/strategy/code/hook.jsonnet diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/hook/hooktest/web_hook_test_server.go b/selfservice/hook/hooktest/web_hook_test_server.go new file mode 100644 index 000000000000..6111b48540a7 --- /dev/null +++ b/selfservice/hook/hooktest/web_hook_test_server.go @@ -0,0 +1,74 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hooktest + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/configx" + "github.com/ory/x/ioutilx" +) + +var jsonnet = base64.StdEncoding.EncodeToString([]byte("function(ctx) ctx")) + +type Server struct { + *httptest.Server + LastBody []byte +} + +// NewServer returns a new webhook server for testing. +func NewServer() *Server { + s := new(Server) + httptestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.LastBody = ioutilx.MustReadAll(r.Body) + w.WriteHeader(http.StatusOK) + })) + + s.Server = httptestServer + + return s +} + +// HookConfig returns the hook configuration for calling the webhook server. +func (s *Server) HookConfig() config.SelfServiceHook { + return config.SelfServiceHook{ + Name: "web_hook", + Config: []byte(fmt.Sprintf(` +{ + "method": "POST", + "url": "%s", + "body": "base64://%s" +}`, s.URL, jsonnet)), + } +} + +func (s *Server) AssertTransientPayload(t *testing.T, expected string) { + require.NotEmpty(t, s.LastBody) + actual := gjson.GetBytes(s.LastBody, "flow.transient_payload").String() + assert.JSONEq(t, expected, actual, "%+v", actual) +} + +// SetConfig adds the webhook to the list of hooks for the given key and restores +// the original configuration after the test. +func (s *Server) SetConfig(t *testing.T, conf *configx.Provider, key string) { + var newValue []config.SelfServiceHook + original := conf.Get(key) + if originalHooks, ok := original.([]config.SelfServiceHook); ok { + newValue = slices.Clone(originalHooks) + } + require.NoError(t, conf.Set(key, append(newValue, s.HookConfig()))) + t.Cleanup(func() { + _ = conf.Set(key, original) + }) +} diff --git a/selfservice/strategy/code/hook.jsonnet b/selfservice/strategy/code/hook.jsonnet deleted file mode 100644 index 54223dda2f32..000000000000 --- a/selfservice/strategy/code/hook.jsonnet +++ /dev/null @@ -1 +0,0 @@ -function(ctx) ctx \ No newline at end of file diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go index a8bea043f91c..98f3d9c7f21c 100644 --- a/selfservice/strategy/code/strategy_recovery_test.go +++ b/selfservice/strategy/code/strategy_recovery_test.go @@ -30,6 +30,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/hook/hooktest" "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/session" "github.com/ory/kratos/text" @@ -294,28 +295,12 @@ func TestRecovery(t *testing.T) { }) t.Run("description=should pass transient data to email template and webhooks", func(t *testing.T) { - var webhookReceivedTransientPayload string - webhookTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - webhookReceivedTransientPayload = gjson.GetBytes(ioutilx.MustReadAll(r.Body), "flow.transient_payload").String() - w.WriteHeader(http.StatusOK) - })) + webhookTS := hooktest.NewServer() t.Cleanup(webhookTS.Close) - conf.MustSet( - ctx, - "selfservice.flows.recovery.after.hooks", - []config.SelfServiceHook{{Name: "web_hook", Config: []byte( - fmt.Sprintf(`{ - "method":"POST", - "url": "%s", - "body":"file://./hook.jsonnet" -}`, webhookTS.URL), - )}}, - ) - - t.Cleanup(func() { - conf.MustSet(ctx, "selfservice.flows.recovery.after.hooks", nil) - }) + conf.MustSet(ctx, "selfservice.flows.recovery.after.hooks", []config.SelfServiceHook{webhookTS.HookConfig()}) + t.Cleanup(func() { conf.MustSet(ctx, "selfservice.flows.recovery.after.hooks", nil) }) + client := testhelpers.NewClientWithCookies(t) email := testhelpers.RandomEmail() createIdentityToRecover(t, reg, email) @@ -347,7 +332,7 @@ func TestRecovery(t *testing.T) { }))) require.NoError(t, err) - assert.JSONEq(t, webhookPayload, webhookReceivedTransientPayload, + assert.JSONEq(t, webhookPayload, gjson.GetBytes(webhookTS.LastBody, "flow.transient_payload").String(), "should pass transient payload to webhook") }) diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 09bb8ab27e6e..42b948ec7c11 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -258,9 +258,10 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } if err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ - State: state.String(), - FlowID: f.ID.String(), - Traits: p.Traits, + State: state.String(), + FlowID: f.ID.String(), + Traits: p.Traits, + TransientPayload: f.TransientPayload, }), continuity.WithLifespan(time.Minute*30)); err != nil { return nil, s.handleError(w, r, f, pid, nil, err) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 951f061995e3..74ea4e0726d6 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/hook/hooktest" "github.com/ory/x/sqlxx" "github.com/ory/kratos/hydra" @@ -457,38 +458,76 @@ func TestStrategy(t *testing.T) { } t.Run("case=register and then login", func(t *testing.T) { + postRegistrationWebhook := hooktest.NewServer() + t.Cleanup(postRegistrationWebhook.Close) + postLoginWebhook := hooktest.NewServer() + t.Cleanup(postLoginWebhook.Close) + + postRegistrationWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + postLoginWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, config.HookGlobal)) + subject = "register-then-login@ory.sh" scope = []string{"openid", "offline"} t.Run("case=should pass registration", func(t *testing.T) { + transientPayload := `{"data": "registration"}` r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{ + "transient_payload": {transientPayload}, + }) assertIdentity(t, res, body) expectTokens(t, "valid", body) assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + postRegistrationWebhook.AssertTransientPayload(t, transientPayload) }) t.Run("case=should pass login", func(t *testing.T) { + transientPayload := `{"data": "login"}` r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{ + "transient_payload": {transientPayload}, + }) assertIdentity(t, res, body) expectTokens(t, "valid", body) assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + postLoginWebhook.AssertTransientPayload(t, transientPayload) }) }) t.Run("case=login without registered account", func(t *testing.T) { + postRegistrationWebhook := hooktest.NewServer() + t.Cleanup(postRegistrationWebhook.Close) + postLoginWebhook := hooktest.NewServer() + t.Cleanup(postLoginWebhook.Close) + + postRegistrationWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + postLoginWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, config.HookGlobal)) + subject = "login-without-register@ory.sh" scope = []string{"openid"} t.Run("case=should pass login", func(t *testing.T) { + transientPayload := `{"data": "login to registration"}` + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{ + "transient_payload": {transientPayload}, + }) assertIdentity(t, res, body) assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + assert.Empty(t, postLoginWebhook.LastBody, + "post login hook should not have been called, because this was a registration flow") + postRegistrationWebhook.AssertTransientPayload(t, transientPayload) }) }) From b132c94e50ec1cf2c4c7496c17df25036380ce53 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:05:24 +0000 Subject: [PATCH 063/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 800f8f1036ef46a561d24dcdec45dd48803978d7 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 4 Apr 2024 14:47:00 +0200 Subject: [PATCH 064/262] fix: don't require connection_uri in SMTP (#3861) --- embedx/config.schema.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 0b540ea25cd6..0a94c54dbb23 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -268,8 +268,8 @@ "description": "The HTTP headers that must be applied to the Web-Hook", "additionalProperties": { "type": "string" - } - }, + } + }, "body": { "type": "string", "oneOf": [ @@ -2059,7 +2059,6 @@ "default": "localhost" } }, - "required": ["connection_uri"], "additionalProperties": false }, "sms": { From 660f330ab69ef0e6fd21501fbc9dfed693d4a715 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Fri, 5 Apr 2024 12:05:23 +0200 Subject: [PATCH 065/262] fix: do not require method to be passkey in settings schema (#3862) --- selfservice/strategy/passkey/.schema/settings.schema.json | 2 +- selfservice/strategy/passkey/passkey_settings.go | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/selfservice/strategy/passkey/.schema/settings.schema.json b/selfservice/strategy/passkey/.schema/settings.schema.json index 7753e441d04f..58bf997024fd 100644 --- a/selfservice/strategy/passkey/.schema/settings.schema.json +++ b/selfservice/strategy/passkey/.schema/settings.schema.json @@ -7,7 +7,7 @@ "type": "string" }, "method": { - "const": "passkey" + "type": "string" }, "passkey_settings_register": { "type": "string" diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go index 05d49ab56f63..548a261e442b 100644 --- a/selfservice/strategy/passkey/passkey_settings.go +++ b/selfservice/strategy/passkey/passkey_settings.go @@ -125,7 +125,8 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity Attributes: &node.InputAttributes{ Name: node.PasskeySettingsRegister, Type: node.InputAttributeTypeHidden, - }}) + }, + }) f.UI.Nodes.Upsert(&node.Node{ Type: node.Input, @@ -135,7 +136,8 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity Name: node.PasskeyCreateData, Type: node.InputAttributeTypeHidden, FieldValue: string(injectWebAuthnOptions), - }}) + }, + }) return nil } @@ -156,7 +158,7 @@ func (s *Strategy) identityListWebAuthn(id *identity.Identity) (*identity.Creden func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { if f.Type != flow.TypeBrowser { - return nil, flow.ErrStrategyNotResponsible + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } var p updateSettingsFlowWithPasskeyMethod ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) From f696fcfb592482df8ad5430b42f8056bcb46a7f0 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:56:34 +0000 Subject: [PATCH 066/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5dabc7b428..26ebee214447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-03-26)](#2024-03-26) +- [ (2024-04-05)](#2024-04-05) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-03-26) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-05) ## Breaking Changes @@ -337,6 +337,12 @@ defaults to `false`. - Add login succeeded event to post registration hook ([#3739](https://github.com/ory/kratos/issues/3739)) ([b685fa5](https://github.com/ory/kratos/commit/b685fa5477be2ba099fd2420b27b2411fafc7e51)) +- Add missing env vars to set up guide + ([#3855](https://github.com/ory/kratos/issues/3855)) + ([da90502](https://github.com/ory/kratos/commit/da90502dc3bf8e3d34fb4ecc531834b1919989ad)): + + Closes https://github.com/ory/kratos/issues/3828 + - Add missing indexes and remove unused index ([6d7372e](https://github.com/ory/kratos/commit/6d7372ee3d88ee4fc552b969dd0ff338dcc0544c)) - Add missing indexes and remove unused index @@ -347,6 +353,17 @@ defaults to `false`. ([b291c95](https://github.com/ory/kratos/commit/b291c959c18c72f5edc55607ab23b4592faf8d53)) - Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) +- Do not require method to be passkey in settings schema + ([#3862](https://github.com/ory/kratos/issues/3862)) + ([660f330](https://github.com/ory/kratos/commit/660f330ab69ef0e6fd21501fbc9dfed693d4a715)) +- Don't require connection_uri in SMTP + ([#3861](https://github.com/ory/kratos/issues/3861)) + ([800f8f1](https://github.com/ory/kratos/commit/800f8f1036ef46a561d24dcdec45dd48803978d7)) +- Don't treat passkeys as AAL2 + ([#3853](https://github.com/ory/kratos/issues/3853)) + ([8eee972](https://github.com/ory/kratos/commit/8eee972d89accb02b3caa053fca2f16ed2c876f1)) +- Drop index if exists ([#3846](https://github.com/ory/kratos/issues/3846)) + ([ad0619d](https://github.com/ory/kratos/commit/ad0619d803cd2842a67c56a545ec5ab252501b0f)) - Drop trigram index on identifiers ([#3827](https://github.com/ory/kratos/issues/3827)) ([8f8fd90](https://github.com/ory/kratos/commit/8f8fd90304886ecd689a85fc60c4712e47526cdd)) @@ -397,11 +414,19 @@ defaults to `false`. user-controlled and these endpoints could not be used fully due to the backend ignoring any value other than `true` (all lowercase). +- Webhook transient payload in OIDC login flows + ([#3857](https://github.com/ory/kratos/issues/3857)) + ([2cdfc70](https://github.com/ory/kratos/commit/2cdfc70c726a166790b98d419895f0396d13176f)): + + - fix: transient payload with OIDC login + ### Features - Add `include_credential` query param to `/admin/identities` list call ([#3343](https://github.com/ory/kratos/issues/3343)) ([d94530a](https://github.com/ory/kratos/commit/d94530a716358895b01b65babd77226fab69f494)) +- Add headers to web hooks ([#3849](https://github.com/ory/kratos/issues/3849)) + ([4642de0](https://github.com/ory/kratos/commit/4642de0cfd1fb15bc48c7093be9449abd488755c)) - Add transient payloads to all flows ([#3738](https://github.com/ory/kratos/issues/3738)) ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) From 6e63d06db1cd1ab62f8a2d0b202ec74572420204 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:14:43 +0200 Subject: [PATCH 067/262] fix: use correct post-verification identity state in post-hooks (#3863) --- internal/client-go/go.sum | 1 + .../testhelpers/selfservice_verification.go | 27 +++++++++++++++++++ .../strategy/code/strategy_verification.go | 18 ++++++------- .../code/strategy_verification_test.go | 14 ++++++++++ .../strategy/link/strategy_verification.go | 18 ++++++------- .../link/strategy_verification_test.go | 13 +++++++++ 6 files changed, 73 insertions(+), 18 deletions(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/testhelpers/selfservice_verification.go b/internal/testhelpers/selfservice_verification.go index 92bc5b191d43..25c40bf32f5e 100644 --- a/internal/testhelpers/selfservice_verification.go +++ b/internal/testhelpers/selfservice_verification.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "io" "net/http" "net/http/httptest" "net/url" @@ -29,6 +30,32 @@ import ( "github.com/ory/kratos/x" ) +func NewVerifyAfterHookWebHookTarget(ctx context.Context, t *testing.T, conf *config.Config, assert func(t *testing.T, body []byte)) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + msg, err := io.ReadAll(r.Body) + require.NoError(t, err) + + assert(t, msg) + })) + + // A hook to ensure that the verification hook is called with the correct data + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{ + { + "hook": "web_hook", + "config": map[string]interface{}{ + "url": ts.URL, + "method": "POST", + "body": "base64://ZnVuY3Rpb24oY3R4KSB7CiAgICBpZGVudGl0eTogY3R4LmlkZW50aXR5Cn0=", + }, + }, + }) + + t.Cleanup(ts.Close) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{}) + }) +} + func NewRecoveryUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server { ctx := context.Background() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index 2f80a4490982..5b30e4c2f5ec 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -254,15 +254,6 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, f.Type, err) } - i, err := s.deps.IdentityPool().GetIdentity(r.Context(), code.VerifiableAddress.IdentityID, identity.ExpandDefault) - if err != nil { - return s.retryVerificationFlowWithError(w, r, f.Type, err) - } - - if err := s.deps.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { - return s.retryVerificationFlowWithError(w, r, f.Type, err) - } - address := code.VerifiableAddress address.Verified = true verifiedAt := sqlxx.NullTime(time.Now().UTC()) @@ -272,6 +263,11 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, f.Type, err) } + i, err := s.deps.IdentityPool().GetIdentity(r.Context(), code.VerifiableAddress.IdentityID, identity.ExpandDefault) + if err != nil { + return s.retryVerificationFlowWithError(w, r, f.Type, err) + } + returnTo := f.ContinueURL(r.Context(), s.deps.Config()) f.UI = &container.Container{ @@ -292,6 +288,10 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) } + if err := s.deps.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { + return s.retryVerificationFlowWithError(w, r, f.Type, err) + } + return nil } diff --git a/selfservice/strategy/code/strategy_verification_test.go b/selfservice/strategy/code/strategy_verification_test.go index 6f25e324d8b2..322dc56eabd2 100644 --- a/selfservice/strategy/code/strategy_verification_test.go +++ b/selfservice/strategy/code/strategy_verification_test.go @@ -13,6 +13,7 @@ import ( "net/http/httptest" "net/url" "strings" + "sync" "testing" "time" @@ -297,6 +298,13 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { + var wg sync.WaitGroup + testhelpers.NewVerifyAfterHookWebHookTarget(ctx, t, conf, func(t *testing.T, msg []byte) { + defer wg.Done() + assert.EqualValues(t, true, gjson.GetBytes(msg, "identity.verifiable_addresses.0.verified").Bool(), string(msg)) + assert.EqualValues(t, "completed", gjson.GetBytes(msg, "identity.verifiable_addresses.0.status").String(), string(msg)) + }) + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) @@ -342,15 +350,21 @@ func TestVerification(t *testing.T) { } t.Run("type=browser", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, false, values)) + wg.Wait() }) t.Run("type=spa", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, true, values)) + wg.Wait() }) t.Run("type=api", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, true, false, values)) + wg.Wait() }) }) diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index 6fe054f746fb..a2a72ea9a277 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -216,15 +216,6 @@ func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) } - i, err := s.d.IdentityPool().GetIdentity(r.Context(), token.VerifiableAddress.IdentityID, identity.ExpandDefault) - if err != nil { - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) - } - - if err := s.d.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) - } - address := token.VerifiableAddress address.Verified = true verifiedAt := sqlxx.NullTime(time.Now().UTC()) @@ -234,6 +225,11 @@ func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) } + i, err := s.d.IdentityPool().GetIdentity(r.Context(), token.VerifiableAddress.IdentityID, identity.ExpandDefault) + if err != nil { + return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) + } + returnTo := f.ContinueURL(r.Context(), s.d.Config()) f.UI. @@ -259,6 +255,10 @@ func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) } + if err := s.d.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { + return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) + } + return nil } diff --git a/selfservice/strategy/link/strategy_verification_test.go b/selfservice/strategy/link/strategy_verification_test.go index cab8fec60b99..84ee51ae9dd1 100644 --- a/selfservice/strategy/link/strategy_verification_test.go +++ b/selfservice/strategy/link/strategy_verification_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "sync" "testing" "time" @@ -270,6 +271,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { + var wg sync.WaitGroup + testhelpers.NewVerifyAfterHookWebHookTarget(ctx, t, conf, func(t *testing.T, msg []byte) { + defer wg.Done() + assert.EqualValues(t, true, gjson.GetBytes(msg, "identity.verifiable_addresses.0.verified").Bool(), string(msg)) + assert.EqualValues(t, "completed", gjson.GetBytes(msg, "identity.verifiable_addresses.0.status").String(), string(msg)) + }) check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) @@ -310,15 +317,21 @@ func TestVerification(t *testing.T) { } t.Run("type=browser", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, false, values)) + wg.Wait() }) t.Run("type=spa", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, true, values)) + wg.Wait() }) t.Run("type=api", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, true, false, values)) + wg.Wait() }) }) From eb67bed1f26d2c7ff10e5481b679b2213b44676d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:16:10 +0000 Subject: [PATCH 068/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 11d221a4d33878930ca7025ae1b5c18b25dd1add Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Tue, 16 Apr 2024 15:23:11 +0200 Subject: [PATCH 069/262] fix: linkedin issuer override (#3875) --- .../strategy/oidc/provider_linkedin_v2.go | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/selfservice/strategy/oidc/provider_linkedin_v2.go b/selfservice/strategy/oidc/provider_linkedin_v2.go index 7ce40239ef46..a71d801c24bd 100644 --- a/selfservice/strategy/oidc/provider_linkedin_v2.go +++ b/selfservice/strategy/oidc/provider_linkedin_v2.go @@ -3,18 +3,6 @@ package oidc -import ( - "context" - "net/url" - - gooidc "github.com/coreos/go-oidc/v3/oidc" - "golang.org/x/oauth2" -) - -type ProviderLinkedInV2 struct { - *ProviderGenericOIDC -} - func NewProviderLinkedInV2( config *Configuration, reg Dependencies, @@ -22,26 +10,8 @@ func NewProviderLinkedInV2( config.ClaimsSource = ClaimsSourceUserInfo config.IssuerURL = "https://www.linkedin.com/oauth" - return &ProviderLinkedInV2{ - ProviderGenericOIDC: &ProviderGenericOIDC{ - config: config, - reg: reg, - }, + return &ProviderGenericOIDC{ + config: config, + reg: reg, } } - -func (l *ProviderLinkedInV2) wrapCtx(ctx context.Context) context.Context { - // We need to overwrite the issuer here because the discovery URL is under - // `https://www.linkedin.com/oauth/.well-known/openid-configuration`, wherease - // the issuer is `https://www.linkedin.com` (without the `/oauth`). This is - // not conformant according to the OIDC spec, but needed for LinkedIn. - return gooidc.InsecureIssuerURLContext(ctx, "https://www.linkedin.com") -} - -func (l *ProviderLinkedInV2) OAuth2(ctx context.Context) (*oauth2.Config, error) { - return l.ProviderGenericOIDC.OAuth2(l.wrapCtx(ctx)) -} - -func (l *ProviderLinkedInV2) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { - return l.ProviderGenericOIDC.Claims(l.wrapCtx(ctx), exchange, query) -} From b96c6a512f0eb2562340b7c64ecd4b4ac74aba21 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:16:00 +0000 Subject: [PATCH 070/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ebee214447..afb90f96a9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-04-05)](#2024-04-05) +- [ (2024-04-16)](#2024-04-16) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-05) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-16) ## Breaking Changes @@ -376,6 +376,8 @@ defaults to `false`. - Improve SDK discriminators ([#3844](https://github.com/ory/kratos/issues/3844)) ([c08b3ad](https://github.com/ory/kratos/commit/c08b3ad76c5adb712c945cdbd92a9a51832e94b9)) +- Linkedin issuer override ([#3875](https://github.com/ory/kratos/issues/3875)) + ([11d221a](https://github.com/ory/kratos/commit/11d221a4d33878930ca7025ae1b5c18b25dd1add)) - Make sure emails can still be sent with SMS enabled ([#3795](https://github.com/ory/kratos/issues/3795)) ([7c68c5a](https://github.com/ory/kratos/commit/7c68c5aa69ed76a84a37a37a3555277ddc772cf8)) @@ -414,6 +416,9 @@ defaults to `false`. user-controlled and these endpoints could not be used fully due to the backend ignoring any value other than `true` (all lowercase). +- Use correct post-verification identity state in post-hooks + ([#3863](https://github.com/ory/kratos/issues/3863)) + ([6e63d06](https://github.com/ory/kratos/commit/6e63d06db1cd1ab62f8a2d0b202ec74572420204)) - Webhook transient payload in OIDC login flows ([#3857](https://github.com/ory/kratos/issues/3857)) ([2cdfc70](https://github.com/ory/kratos/commit/2cdfc70c726a166790b98d419895f0396d13176f)): From 6b275f35a0732ffb723d47df5b6afbdc06eaf71f Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 16 Apr 2024 17:02:15 +0200 Subject: [PATCH 071/262] test: deflake session test (#3864) --- session/handler_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/session/handler_test.go b/session/handler_test.go index cd0a9ddca6c1..906a985e7356 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -187,7 +187,14 @@ func TestSessionWhoAmI(t *testing.T) { if maxAge > 0 { assert.Equal(t, fmt.Sprintf("%0.f", maxAge.Seconds()), res.Header.Get("Ory-Session-Cache-For")) } else { - assert.Equal(t, fmt.Sprintf("%0.f", conf.SessionLifespan(ctx).Seconds()), res.Header.Get("Ory-Session-Cache-For")) + // parse int to string from Ory-Session-Cache-For + parsed, err := strconv.Atoi(res.Header.Get("Ory-Session-Cache-For")) + require.NoError(t, err) + lifespan := conf.SessionLifespan(ctx).Seconds() + // We need to account for the time it takes to make the request, as depending on the system it might take a few more ms which leads to the value being off by a second or more. + assert.Condition(t, func() bool { + return parsed > int(lifespan-5) && parsed <= int(lifespan) + }, "Expected the value of the Ory-Session-Cache-For header to be roughly around the configured lifespan. Got parsed: %d, lifespan: %d", parsed, int(lifespan)) } } else { assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) From 386078e0b5c74c54ce2c7dc6fd12fd865817b87a Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 16 Apr 2024 22:54:33 +0200 Subject: [PATCH 072/262] feat: add session to post login webhook (#3877) --- selfservice/hook/stub/test_body.jsonnet | 1 + selfservice/hook/web_hook.go | 2 ++ selfservice/hook/web_hook_integration_test.go | 20 ++++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/selfservice/hook/stub/test_body.jsonnet b/selfservice/hook/stub/test_body.jsonnet index 409fa13695ca..117ecc587707 100644 --- a/selfservice/hook/stub/test_body.jsonnet +++ b/selfservice/hook/stub/test_body.jsonnet @@ -1,6 +1,7 @@ function(ctx) std.prune({ flow_id: ctx.flow.id, identity_id: if std.objectHas(ctx, "identity") then ctx.identity.id, + session_id: if std.objectHas(ctx, "session") then ctx.session.id, headers: ctx.request_headers, url: ctx.request_url, method: ctx.request_method, diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index cbb9f0a0b0d4..d4c8131e50d5 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -81,6 +81,7 @@ type ( RequestURL string `json:"request_url"` RequestCookies map[string]string `json:"request_cookies"` Identity *identity.Identity `json:"identity,omitempty"` + Session *session.Session `json:"session,omitempty"` } WebHook struct { @@ -140,6 +141,7 @@ func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, RequestURL: x.RequestURL(req).String(), RequestCookies: cookies(req), Identity: session.Identity, + Session: session, }) }) } diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index c3d8344fc7a8..59159f49b6cf 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -148,6 +148,24 @@ func TestWebHooks(t *testing.T) { }`, f.GetID(), s.Identity.ID, string(h), req.Method, "http://www.ory.sh/some_end_point", string(tp)) } + bodyWithFlowAndIdentityAndSessionAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string { + h, _ := json.Marshal(req.Header) + return fmt.Sprintf(`{ + "flow_id": "%s", + "identity_id": "%s", + "session_id": "%s", + "headers": %s, + "method": "%s", + "url": "%s", + "cookies": { + "Some-Cookie-1": "Some-Cookie-Value", + "Some-Cookie-2": "Some-other-Cookie-Value", + "Some-Cookie-3": "Third-Cookie-Value" + }, + "transient_payload": %s + }`, f.GetID(), s.Identity.ID, s.ID, string(h), req.Method, "http://www.ory.sh/some_end_point", string(tp)) + } + for _, tc := range []struct { uc string callWebHook func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error @@ -171,7 +189,7 @@ func TestWebHooks(t *testing.T) { return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) + return bodyWithFlowAndIdentityAndSessionAndTransientPayload(req, f, s, transientPayload) }, }, { From 9fa25b57ad80d3dcbc40b82be75177ab7d938f18 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:45:31 +0000 Subject: [PATCH 073/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afb90f96a9fb..454cb471873c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -432,6 +432,9 @@ defaults to `false`. ([d94530a](https://github.com/ory/kratos/commit/d94530a716358895b01b65babd77226fab69f494)) - Add headers to web hooks ([#3849](https://github.com/ory/kratos/issues/3849)) ([4642de0](https://github.com/ory/kratos/commit/4642de0cfd1fb15bc48c7093be9449abd488755c)) +- Add session to post login webhook + ([#3877](https://github.com/ory/kratos/issues/3877)) + ([386078e](https://github.com/ory/kratos/commit/386078e0b5c74c54ce2c7dc6fd12fd865817b87a)) - Add transient payloads to all flows ([#3738](https://github.com/ory/kratos/issues/3738)) ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) @@ -464,6 +467,8 @@ defaults to `false`. ### Tests +- Deflake session test ([#3864](https://github.com/ory/kratos/issues/3864)) + ([6b275f3](https://github.com/ory/kratos/commit/6b275f35a0732ffb723d47df5b6afbdc06eaf71f)) - Resolve failing test for empty tokens ([#3775](https://github.com/ory/kratos/issues/3775)) ([7277368](https://github.com/ory/kratos/commit/7277368bc28df8f0badffc7e739cef20f05e9a02)) From e94250705e999567e2ed58cebdb3f6a9d589e3ef Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Wed, 17 Apr 2024 13:30:04 +0200 Subject: [PATCH 074/262] fix: always issue session last (#3876) In post persist hooks, the session issuance hook always needs to come last. This fixes the getHooks function to ensure this. --- driver/registry_default_hooks.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index 05b9a10f3d98..73a855daadc5 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -62,10 +62,12 @@ func (m *RegistryDefault) WithHooks(hooks map[string]func(config.SelfServiceHook } func (m *RegistryDefault) getHooks(credentialsType string, configs []config.SelfServiceHook) (i []interface{}) { + var addSessionIssuer bool for _, h := range configs { switch h.Name { case hook.KeySessionIssuer: - i = append(i, m.HookSessionIssuer()) + // The session issuer hook always needs to come last. + addSessionIssuer = true case hook.KeySessionDestroyer: i = append(i, m.HookSessionDestroyer()) case hook.KeyWebHook: @@ -96,6 +98,9 @@ func (m *RegistryDefault) getHooks(credentialsType string, configs []config.Self Errorf("A unknown hook was requested and can therefore not be used") } } + if addSessionIssuer { + i = append(i, m.HookSessionIssuer()) + } return i } From da51dcdb8c82a5dbd290ab2f48ad74a1c6dd18f0 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Wed, 17 Apr 2024 16:52:32 +0200 Subject: [PATCH 075/262] fix: tweaks to UpsertSessions (#3878) --- persistence/sql/persister_session.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go index 7cbd968f50a2..c0c1c3d865ba 100644 --- a/persistence/sql/persister_session.go +++ b/persistence/sql/persister_session.go @@ -185,10 +185,21 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err s.NID = p.NetworkID(ctx) - return errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + var updated bool + defer func() { + if err != nil { + return + } + if updated { + trace.SpanFromContext(ctx).AddEvent(events.NewSessionChanged(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) + } else { + trace.SpanFromContext(ctx).AddEvent(events.NewSessionIssued(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) + } + }() + return errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { + updated = false exists := false if !s.ID.IsNil() { - var err error exists, err = tx.Where("id = ? AND nid = ?", s.ID, s.NID).Exists(new(session.Session)) if err != nil { return sqlcon.HandleError(err) @@ -198,10 +209,10 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err if exists { // This must not be eager or identities will be created / updated // Only update session and not corresponding session device records - if err := tx.Update(s); err != nil { + if err := tx.Update(s, "issued_at", "identity_id", "nid"); err != nil { return sqlcon.HandleError(err) } - trace.SpanFromContext(ctx).AddEvent(events.NewSessionChanged(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) + updated = true return nil } @@ -227,7 +238,6 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err } } - trace.SpanFromContext(ctx).AddEvent(events.NewSessionIssued(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) return nil })) } From 31f77b85348bede2ea1c785d627006be1ea36c87 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:42:49 +0000 Subject: [PATCH 076/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 454cb471873c..938cff99f429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-04-16)](#2024-04-16) +- [ (2024-04-17)](#2024-04-17) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-16) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-17) ## Breaking Changes @@ -351,6 +351,12 @@ defaults to `false`. - Add sms mfa via parameter to spec ([#3766](https://github.com/ory/kratos/issues/3766)) ([b291c95](https://github.com/ory/kratos/commit/b291c959c18c72f5edc55607ab23b4592faf8d53)) +- Always issue session last ([#3876](https://github.com/ory/kratos/issues/3876)) + ([e942507](https://github.com/ory/kratos/commit/e94250705e999567e2ed58cebdb3f6a9d589e3ef)): + + In post persist hooks, the session issuance hook always needs to come last. + This fixes the getHooks function to ensure this. + - Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) - Do not require method to be passkey in settings schema @@ -416,6 +422,8 @@ defaults to `false`. user-controlled and these endpoints could not be used fully due to the backend ignoring any value other than `true` (all lowercase). +- Tweaks to UpsertSessions ([#3878](https://github.com/ory/kratos/issues/3878)) + ([da51dcd](https://github.com/ory/kratos/commit/da51dcdb8c82a5dbd290ab2f48ad74a1c6dd18f0)) - Use correct post-verification identity state in post-hooks ([#3863](https://github.com/ory/kratos/issues/3863)) ([6e63d06](https://github.com/ory/kratos/commit/6e63d06db1cd1ab62f8a2d0b202ec74572420204)) From 696cc1b59b18627fec63915070f4d8c5b3e3250d Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Wed, 17 Apr 2024 19:16:32 +0200 Subject: [PATCH 077/262] fix: allow updating just the verified_at timestamp of addresses (#3880) --- identity/handler_test.go | 92 +++++++++++++++++++ identity/identity_verification.go | 2 +- identity/identity_verification_test.go | 9 +- .../sql/identity/persister_identity.go | 3 + 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/identity/handler_test.go b/identity/handler_test.go index bfdf1a86dfc3..ab2ed0c0cbdc 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -928,6 +928,98 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=PATCH should update verified_at timestamp", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + email := x.NewUUID().String() + "@ory.sh" + var cr identity.CreateIdentityBody + cr.SchemaID = "employee" + cr.Traits = []byte(`{"email":"` + email + `"}`) + res := send(t, ts, "POST", "/identities", http.StatusCreated, &cr) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified_at").Exists(), "%s", res.Raw) + identityID := res.Get("id").String() + + // set to verified, should also update verified_at timestamp + patch1 := []patch{ + { + "op": "replace", + "path": "/verifiable_addresses/0/verified", + "value": true, + }, + } + + now := time.Now() + + res = send(t, ts, "PATCH", "/identities/"+identityID, http.StatusOK, &patch1) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.verified_at").Time(), 5*time.Second, "%s", res.Raw) + + res = get(t, ts, "/identities/"+identityID, http.StatusOK) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.verified_at").Time(), 5*time.Second, "%s", res.Raw) + + // update only verified_at timestamp + verifiedAt := time.Date(1999, 1, 7, 8, 23, 19, 0, time.UTC) + patch2 := []patch{ + { + "op": "replace", + "path": "/verifiable_addresses/0/verified_at", + "value": verifiedAt.Format(time.RFC3339), + }, + } + + now = time.Now() + res = send(t, ts, "PATCH", "/identities/"+identityID, http.StatusOK, &patch2) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Equalf(t, verifiedAt, res.Get("verifiable_addresses.0.verified_at").Time(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + + res = get(t, ts, "/identities/"+identityID, http.StatusOK) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Equalf(t, verifiedAt, res.Get("verifiable_addresses.0.verified_at").Time(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + + // remove verified status + patch3 := []patch{ + { + "op": "replace", + "path": "/verifiable_addresses/0/verified", + "value": false, + }, + } + + now = time.Now() + + res = send(t, ts, "PATCH", "/identities/"+identityID, http.StatusOK, &patch3) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified_at").Exists(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + + res = get(t, ts, "/identities/"+identityID, http.StatusOK) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified_at").Exists(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + }) + } + }) + t.Run("case=PATCH update should not persist if schema id is invalid", func(t *testing.T) { uuid := x.NewUUID().String() i := &identity.Identity{Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, uuid))} diff --git a/identity/identity_verification.go b/identity/identity_verification.go index 54ac435ec9fd..251fee019d3b 100644 --- a/identity/identity_verification.go +++ b/identity/identity_verification.go @@ -118,5 +118,5 @@ func (a VerifiableAddress) ValidateNID() error { // Hash returns a unique string representation for the recovery address. func (a VerifiableAddress) Hash() string { - return fmt.Sprintf("%v|%v|%v|%v|%v|%v", a.Value, a.Verified, a.Via, a.Status, a.IdentityID, a.NID) + return fmt.Sprintf("%v|%v|%v|%v|%v|%v|%v", a.Value, a.Verified, a.Via, a.Status, a.VerifiedAt, a.IdentityID, a.NID) } diff --git a/identity/identity_verification_test.go b/identity/identity_verification_test.go index 4559a5759dba..6f3ca2c69524 100644 --- a/identity/identity_verification_test.go +++ b/identity/identity_verification_test.go @@ -34,10 +34,10 @@ func TestNewVerifiableEmailAddress(t *testing.T) { } var tagsIgnoredForHashing = map[string]struct{}{ - "id": {}, - "created_at": {}, - "updated_at": {}, - "verified_at": {}, + "id": {}, + "created_at": {}, + "updated_at": {}, + // "verified_at": {}, // we explicitly want to be able to update just this field and nothing else } func reflectiveHash(record any) string { @@ -102,5 +102,4 @@ func TestVerifiableAddress_Hash(t *testing.T) { ) }) } - } diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index 8c001bc87e6e..ea532485944e 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -477,6 +477,9 @@ func (p *IdentityPersister) normalizeVerifiableAddresses(ctx context.Context, id if v.Verified && (v.VerifiedAt == nil || time.Time(*v.VerifiedAt).IsZero()) { v.VerifiedAt = pointerx.Ptr(sqlxx.NullTime(time.Now())) } + if !v.Verified { + v.VerifiedAt = nil + } id.VerifiableAddresses[k] = v } From ddbea202be63067cc0cc8a8d4d2fc503cecf9743 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 17 Apr 2024 18:06:17 +0000 Subject: [PATCH 078/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 938cff99f429..cc022b46418a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -351,6 +351,9 @@ defaults to `false`. - Add sms mfa via parameter to spec ([#3766](https://github.com/ory/kratos/issues/3766)) ([b291c95](https://github.com/ory/kratos/commit/b291c959c18c72f5edc55607ab23b4592faf8d53)) +- Allow updating just the verified_at timestamp of addresses + ([#3880](https://github.com/ory/kratos/issues/3880)) + ([696cc1b](https://github.com/ory/kratos/commit/696cc1b59b18627fec63915070f4d8c5b3e3250d)) - Always issue session last ([#3876](https://github.com/ory/kratos/issues/3876)) ([e942507](https://github.com/ory/kratos/commit/e94250705e999567e2ed58cebdb3f6a9d589e3ef)): From e06c241ffe3f0e696bb1cbc1d1080f9d4e09fbd2 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Mon, 22 Apr 2024 14:27:31 +0200 Subject: [PATCH 079/262] fix: include all creds in duplicate credential err (#3881) --- identity/manager.go | 43 +++++++++++-------- identity/manager_test.go | 37 +++++++++++++++- internal/client-go/go.sum | 1 + selfservice/strategy/oidc/strategy_test.go | 2 +- .../strategy/webauthn/registration_test.go | 2 +- session/handler_test.go | 13 ++---- text/message_validation.go | 23 +++++----- 7 files changed, 81 insertions(+), 40 deletions(-) diff --git a/identity/manager.go b/identity/manager.go index 9bd02ce7451c..c5ab32bbe293 100644 --- a/identity/manager.go +++ b/identity/manager.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "reflect" + "slices" "sort" "go.opentelemetry.io/otel/trace" @@ -188,6 +189,8 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi return creds[i].Type < creds[j].Type }) + duplicateCredErr := &ErrDuplicateCredentials{error: e} + for _, cred := range creds { if cred.Config == nil { continue @@ -202,11 +205,8 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi if len(cred.Identifiers) > 0 { identifierHint = cred.Identifiers[0] } - return &ErrDuplicateCredentials{ - error: e, - availableCredentials: []CredentialsType{cred.Type}, - identifierHint: identifierHint, - } + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.SetIdentifierHint(identifierHint) case CredentialsTypeOIDC: var cfg CredentialsOIDC if err := json.Unmarshal(cred.Config, &cfg); err != nil { @@ -218,12 +218,9 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi available = append(available, provider.Provider) } - return &ErrDuplicateCredentials{ - error: e, - availableCredentials: []CredentialsType{cred.Type}, - availableOIDCProviders: available, - identifierHint: foundConflictAddress, - } + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.SetIdentifierHint(foundConflictAddress) + duplicateCredErr.availableOIDCProviders = available case CredentialsTypeWebAuthn: var cfg CredentialsWebAuthnConfig if err := json.Unmarshal(cred.Config, &cfg); err != nil { @@ -237,18 +234,15 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi for _, webauthn := range cfg.Credentials { if webauthn.IsPasswordless { - return &ErrDuplicateCredentials{ - error: e, - availableCredentials: []CredentialsType{cred.Type}, - identifierHint: identifierHint, - } + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.SetIdentifierHint(identifierHint) + break } } } } - // Still not found? Return generic error. - return &ErrDuplicateCredentials{error: e} + return duplicateCredErr } type ErrDuplicateCredentials struct { @@ -265,15 +259,28 @@ func (e *ErrDuplicateCredentials) Unwrap() error { return e.error } +func (e *ErrDuplicateCredentials) AddCredentialsType(ct CredentialsType) { + e.availableCredentials = append(e.availableCredentials, ct) +} + +func (e *ErrDuplicateCredentials) SetIdentifierHint(hint string) { + if hint != "" { + e.identifierHint = hint + } +} + func (e *ErrDuplicateCredentials) AvailableCredentials() []string { res := make([]string, len(e.availableCredentials)) for k, v := range e.availableCredentials { res[k] = string(v) } + slices.Sort(res) + return res } func (e *ErrDuplicateCredentials) AvailableOIDCProviders() []string { + slices.Sort(e.availableOIDCProviders) return e.availableOIDCProviders } diff --git a/identity/manager_test.go b/identity/manager_test.go index 81001c9ba9c6..5eb3e61aa58c 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -222,6 +222,41 @@ func TestManager(t *testing.T) { assert.Len(t, verr.AvailableOIDCProviders(), 0) assert.Equal(t, verr.IdentifierHint(), email) }) + + t.Run("type=password+oidc+webauthn", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + creds := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + // Identifiers in OIDC are not email addresses, but a unique user ID. + Identifiers: []string{"google:" + uuid.Must(uuid.NewV4()).String()}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider": "google"},{"provider": "github"}]}`), + }, + identity.CredentialsTypeWebAuthn: { + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"credentials": [{"is_passwordless":true}]}`), + }, + } + + first := createIdentity(email, "email_creds", creds) + require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(context.Background(), second) + require.Error(t, err) + + var verr = new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.ElementsMatch(t, []string{"password", "oidc", "webauthn"}, verr.AvailableCredentials()) + assert.ElementsMatch(t, []string{"google", "github"}, verr.AvailableOIDCProviders()) + assert.Equal(t, email, verr.IdentifierHint()) + }) }) runAddress := func(t *testing.T, field string) { @@ -278,7 +313,7 @@ func TestManager(t *testing.T) { var verr = new(identity.ErrDuplicateCredentials) assert.ErrorAs(t, err, &verr) assert.EqualValues(t, []string{identity.CredentialsTypeOIDC.String()}, verr.AvailableCredentials()) - assert.EqualValues(t, verr.AvailableOIDCProviders(), []string{"google", "github"}) + assert.EqualValues(t, verr.AvailableOIDCProviders(), []string{"github", "google"}) assert.Equal(t, verr.IdentifierHint(), email) }) } diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 74ea4e0726d6..8c6b438a9a57 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -1260,7 +1260,7 @@ func TestStrategy(t *testing.T) { var linkingLoginFlow struct{ ID string } t.Run("step=should fail login and start a new login", func(t *testing.T) { res, body := loginWithOIDC(t, client, loginFlow.ID, "valid") - assertUIError(t, res, body, "You tried signing in with existing-oidc-identity-1@ory.sh which is already in use by another account. You can sign in using social sign in. You can sign in using one of the following social sign in providers: Secondprovider.") + assertUIError(t, res, body, "You tried signing in with existing-oidc-identity-1@ory.sh which is already in use by another account. You can sign in using social sign in, or your password. You can sign in using one of the following social sign in providers: Secondprovider.") linkingLoginFlow.ID = gjson.GetBytes(body, "id").String() assert.NotEqual(t, loginFlow.ID.String(), linkingLoginFlow.ID, "should have started a new flow") }) diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index 035e7ffd984c..c65a2c1ec030 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -438,7 +438,7 @@ func TestRegistration(t *testing.T) { actual, _, _ = makeRegistration(t, f, values(email)) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username") - assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your password.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your password, or your passkey or a security key.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) }) } }) diff --git a/session/handler_test.go b/session/handler_test.go index 906a985e7356..3c61b7764832 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -184,18 +184,13 @@ func TestSessionWhoAmI(t *testing.T) { assert.NotEmpty(t, res.Header.Get("X-Kratos-Authenticated-Identity-Id")) if cacheEnabled { + var expectedSeconds int if maxAge > 0 { - assert.Equal(t, fmt.Sprintf("%0.f", maxAge.Seconds()), res.Header.Get("Ory-Session-Cache-For")) + expectedSeconds = int(maxAge.Seconds()) } else { - // parse int to string from Ory-Session-Cache-For - parsed, err := strconv.Atoi(res.Header.Get("Ory-Session-Cache-For")) - require.NoError(t, err) - lifespan := conf.SessionLifespan(ctx).Seconds() - // We need to account for the time it takes to make the request, as depending on the system it might take a few more ms which leads to the value being off by a second or more. - assert.Condition(t, func() bool { - return parsed > int(lifespan-5) && parsed <= int(lifespan) - }, "Expected the value of the Ory-Session-Cache-For header to be roughly around the configured lifespan. Got parsed: %d, lifespan: %d", parsed, int(lifespan)) + expectedSeconds = int(conf.SessionLifespan(ctx).Seconds()) } + assert.InDelta(t, expectedSeconds, x.Must(strconv.Atoi(res.Header.Get("Ory-Session-Cache-For"))), 5) } else { assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) } diff --git a/text/message_validation.go b/text/message_validation.go index 28396180c0c5..b3dff02c3234 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -9,8 +9,6 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" - - "golang.org/x/exp/maps" ) func NewValidationErrorGeneric(reason string) *Message { @@ -279,25 +277,30 @@ func NewErrorValidationDuplicateCredentialsWithHints(availableCredentialTypes [] reason := fmt.Sprintf("You tried signing in with %s which is already in use by another account.", identifier) if len(availableCredentialTypes) > 0 { - humanReadable := make(map[string]struct{}, len(availableCredentialTypes)) + humanReadable := make([]string, 0, len(availableCredentialTypes)) for _, cred := range availableCredentialTypes { switch cred { case "password": - humanReadable["your password"] = struct{}{} + humanReadable = append(humanReadable, "your password") case "oidc": - humanReadable["social sign in"] = struct{}{} + humanReadable = append(humanReadable, "social sign in") case "webauthn": - humanReadable["your PassKey or a security key"] = struct{}{} + humanReadable = append(humanReadable, "your passkey or a security key") } } if len(humanReadable) == 0 { // show at least some hint // also our example message generation tool runs into this case - for _, cred := range availableCredentialTypes { - humanReadable[cred] = struct{}{} - } + humanReadable = append(humanReadable, availableCredentialTypes...) + } + + // Final format: "You can sign in using foo, bar, or baz." + if len(humanReadable) > 1 { + humanReadable[len(humanReadable)-1] = "or " + humanReadable[len(humanReadable)-1] + } + if len(humanReadable) > 0 { + reason += fmt.Sprintf(" You can sign in using %s.", strings.Join(humanReadable, ", ")) } - reason += fmt.Sprintf(" You can sign in using %s.", strings.Join(maps.Keys(humanReadable), ", ")) } if len(oidcProviders) > 0 { reason += fmt.Sprintf(" You can sign in using one of the following social sign in providers: %s.", strings.Join(oidcProviders, ", ")) From 473e17c69968c4e3862adaed476c5d4b6227bf34 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:28:58 +0000 Subject: [PATCH 080/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 9c69ef23ec697ed607edeae50622fdd4cd932e14 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:17:40 +0000 Subject: [PATCH 081/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc022b46418a..f1ce844b9ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-04-17)](#2024-04-17) +- [ (2024-04-22)](#2024-04-22) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-17) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-22) ## Breaking Changes @@ -385,6 +385,9 @@ defaults to `false`. - Improve SDK discriminators ([#3844](https://github.com/ory/kratos/issues/3844)) ([c08b3ad](https://github.com/ory/kratos/commit/c08b3ad76c5adb712c945cdbd92a9a51832e94b9)) +- Include all creds in duplicate credential err + ([#3881](https://github.com/ory/kratos/issues/3881)) + ([e06c241](https://github.com/ory/kratos/commit/e06c241ffe3f0e696bb1cbc1d1080f9d4e09fbd2)) - Linkedin issuer override ([#3875](https://github.com/ory/kratos/issues/3875)) ([11d221a](https://github.com/ory/kratos/commit/11d221a4d33878930ca7025ae1b5c18b25dd1add)) - Make sure emails can still be sent with SMS enabled From 63d785e5e73ff067ec804ecc2107fac1525d3688 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 23 Apr 2024 11:31:18 +0200 Subject: [PATCH 082/262] fix: enum type of session expandables (#3891) --- identity/handler.go | 6 +++--- package-lock.json | 1 - session/handler.go | 12 ++++++++++-- spec/api.json | 8 ++++---- spec/swagger.json | 12 ++++++------ 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index 8622a2e76d8e..00f4a011e8d0 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -896,18 +896,18 @@ func (h *Handler) patch(w http.ResponseWriter, r *http.Request, ps httprouter.Pa patchedIdentity.StateChangedAt = &stateChangedAt } - updatedIdenty := Identity(patchedIdentity) + updatedIdentity := Identity(patchedIdentity) if err := h.r.IdentityManager().Update( r.Context(), - &updatedIdenty, + &updatedIdentity, ManagerAllowWriteProtectedTraits, ); err != nil { h.r.Writer().WriteError(w, r, err) return } - h.r.Writer().Write(w, r, WithCredentialsMetadataAndAdminMetadataInJSON(updatedIdenty)) + h.r.Writer().Write(w, r, WithCredentialsMetadataAndAdminMetadataInJSON(updatedIdentity)) } func deletCredentialWebAuthFromIdentity(identity *Identity) (*Identity, error) { diff --git a/package-lock.json b/package-lock.json index 8f860f034e72..705526d800d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "kratos", "dependencies": { "@openapitools/openapi-generator-cli": "2.7.0", "yamljs": "0.3.0" diff --git a/session/handler.go b/session/handler.go index 3d9d0787404f..73ac5c32ec1b 100644 --- a/session/handler.go +++ b/session/handler.go @@ -338,11 +338,19 @@ type listSessionsRequest struct { // If no value is provided, the expandable properties are skipped. // // required: false - // enum: identity,devices // in: query - ExpandOptions []string `json:"expand"` + ExpandOptions []ListSessionExpandable `json:"expand"` } +// Expandable properties of a session +// swagger:enum ListSessionExpandable +type ListSessionExpandable string + +const ( + ListSessionExpandableIdentity ListSessionExpandable = "identity" + ListSessionExpandableDevices ListSessionExpandable = "devices" +) + // Session List Response // // The response given when listing sessions in an administrative context. diff --git a/spec/api.json b/spec/api.json index 76036e26595f..5155b2e37554 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4785,11 +4785,11 @@ "in": "query", "name": "expand", "schema": { - "enum": [ - "identity", - "devices" - ], "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "type": "array" diff --git a/spec/swagger.json b/spec/swagger.json index 7da943e9f0a8..1f5ce08e8d29 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1039,12 +1039,12 @@ "in": "query" }, { - "enum": [ - "identity", - "devices" - ], "type": "array", "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "description": "ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session.\nIf no value is provided, the expandable properties are skipped.", @@ -3251,9 +3251,9 @@ "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, "NullTime": { - "description": "NullTime implements the Scanner interface so\nit can be used as a scan destination, similar to NullString.", + "description": "NullTime implements the [Scanner] interface so\nit can be used as a scan destination, similar to [NullString].", "type": "object", - "title": "NullTime represents a time.Time that may be null.", + "title": "NullTime represents a [time.Time] that may be null.", "properties": { "Time": { "type": "string", From 0b6f91e6716257865798f0271916afb57b6002c9 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:32:56 +0000 Subject: [PATCH 083/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- spec/swagger.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/swagger.json b/spec/swagger.json index 1f5ce08e8d29..fc38a382e4c8 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3251,9 +3251,9 @@ "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, "NullTime": { - "description": "NullTime implements the [Scanner] interface so\nit can be used as a scan destination, similar to [NullString].", + "description": "NullTime implements the Scanner interface so\nit can be used as a scan destination, similar to NullString.", "type": "object", - "title": "NullTime represents a [time.Time] that may be null.", + "title": "NullTime represents a time.Time that may be null.", "properties": { "Time": { "type": "string", From 17f9a4fe911caa80466bac334c69beb99feb38c2 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 23 Apr 2024 11:45:13 +0200 Subject: [PATCH 084/262] chore: render CLI doc messages into their own *.md file in docs (#3886) --- cmd/clidoc/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go index 5c2555eaddb2..6ed8df8d1748 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -199,7 +199,7 @@ func main() { } } - if err := writeMessages(filepath.Join(os.Args[2], "concepts/ui-user-interface.mdx"), sortedMessages); err != nil { + if err := writeMessages(filepath.Join(os.Args[2], "concepts/ui-messages.md"), sortedMessages); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to generate message table: %+v\n", err) os.Exit(1) } From e8f1bcb1342af994b8e08282aa4066ee00ffe7d4 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Thu, 25 Apr 2024 09:45:20 +0200 Subject: [PATCH 085/262] fix: respect return_to in OIDC API flow error case (#3893) * fix: respect return_to in OIDC API flow error case This fix ensures that we redirect the user to the return_to URL when an error occurs during the OIDC login for native flows. Native flows are initialized through the API, and the browser URL is retrieved from a 422 response after a POST to submit the login flow. Successful OIDC flows already returned the `code` to the `return_to` URL. Now, unsuccessful flows return the `flow` with the current flow ID (which might have changed), so that the caller can retrieve the full flow and act accordingly. * fix: ignore trivvy CVE report Bump in distroless is still open --- .trivyignore | 1 + internal/client-go/go.sum | 1 + selfservice/flow/login/handler_test.go | 15 ++++-- selfservice/strategy/oidc/strategy.go | 19 +++++++- selfservice/strategy/oidc/strategy_test.go | 55 ++++++++++++++++------ 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/.trivyignore b/.trivyignore index a142ea336cff..4a01119a556c 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,2 +1,3 @@ CVE-2022-30065 +CVE-2024-2961 CVE-2023-2650 diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index 813a0c8ad669..c8d5ac97772e 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -547,12 +547,19 @@ func TestFlowLifecycle(t *testing.T) { }) t.Run("case=returns session exchange code with any truthy value", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh", "https://example.com"}) parameters := []string{"true", "True", "1"} - for i := range parameters { - res, body := initFlow(t, url.Values{"return_session_token_exchange_code": {parameters[i]}}, true) - assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) - assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + for _, param := range parameters { + t.Run("return_session_token_exchange_code="+param, func(t *testing.T) { + res, body := initFlow(t, url.Values{ + "return_session_token_exchange_code": {param}, + "return_to": {"https://example.com/redirect"}, + }, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + assert.Equal(t, "https://example.com/redirect", gjson.GetBytes(body, "return_to").String()) + }) } }) diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index b710d7423c85..449bfece3878 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -370,7 +370,7 @@ func (s *Strategy) alreadyAuthenticated(w http.ResponseWriter, r *http.Request, returnTo := s.d.Config().SelfServiceBrowserDefaultReturnTo(ctx) if redirecter, ok := f.(flow.FlowWithRedirect); ok { r, err := x.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(ctx, s.d)...) - if err != nil { + if err == nil { returnTo = r } } @@ -462,6 +462,9 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt case *login.Flow: a.TransientPayload = cntnr.TransientPayload if ff, err := s.processLogin(w, r, a, et, claims, provider, cntnr); err != nil { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } if ff != nil { s.forwardError(w, r, ff, err) return @@ -631,7 +634,19 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl return err } // return a new login flow with the error message embedded in the login flow. - redirectURL := lf.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())) + var redirectURL *url.URL + if lf.Type == flow.TypeAPI { + returnTo := s.d.Config().SelfServiceBrowserDefaultReturnTo(r.Context()) + if redirecter, ok := f.(flow.FlowWithRedirect); ok { + secureReturnTo, err := x.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(r.Context(), s.d)...) + if err == nil { + returnTo = secureReturnTo + } + } + redirectURL = lf.AppendTo(returnTo) + } else { + redirectURL = lf.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())) + } if dc, err := flow.DuplicateCredentials(lf); err == nil && dc != nil { redirectURL = urlx.CopyWithQuery(redirectURL, url.Values{"no_org_ui": {"true"}}) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 8c6b438a9a57..0b6d068b21fc 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -184,7 +184,7 @@ func TestStrategy(t *testing.T) { return res, body } - makeAPICodeFlowRequest := func(t *testing.T, provider, action string) (returnToCode string) { + makeAPICodeFlowRequest := func(t *testing.T, provider, action string) (returnToURL *url.URL) { res, err := testhelpers.NewDebugClient(t).Post(action, "application/json", strings.NewReader(fmt.Sprintf(`{ "method": "oidc", "provider": %q @@ -197,13 +197,10 @@ func TestStrategy(t *testing.T) { res, err = testhelpers.NewClientWithCookieJar(t, nil, true).Get(changeLocation.RedirectBrowserTo) require.NoError(t, err) - returnToURL := res.Request.URL + returnToURL = res.Request.URL assert.True(t, strings.HasPrefix(returnToURL.String(), returnTS.URL+"/app_code")) - code := returnToURL.Query().Get("code") - assert.NotEmpty(t, code, "code query param was empty in the return_to URL") - - return code + return returnToURL } exchangeCodeForToken := func(t *testing.T, codes sessiontokenexchange.Codes) (codeResponse session.CodeExchangeResponse, err error) { @@ -553,12 +550,15 @@ func TestStrategy(t *testing.T) { t.Run("suite=API with session token exchange code", func(t *testing.T) { scope = []string{"openid"} - loginOrRegister := func(t *testing.T, id uuid.UUID, code string) { + loginOrRegister := func(t *testing.T, flowID uuid.UUID, code string) { _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: code}) require.Error(t, err) - action := assertFormValues(t, id, "valid") - returnToCode := makeAPICodeFlowRequest(t, "valid", action) + action := assertFormValues(t, flowID, "valid") + returnToURL := makeAPICodeFlowRequest(t, "valid", action) + returnToCode := returnToURL.Query().Get("code") + assert.NotEmpty(t, code, "code query param was empty in the return_to URL") + codeResponse, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{ InitCode: code, ReturnToCode: returnToCode, @@ -568,11 +568,11 @@ func TestStrategy(t *testing.T) { assert.NotEmpty(t, codeResponse.Token) assert.Equal(t, subject, gjson.GetBytes(codeResponse.Session.Identity.Traits, "subject").String()) } - register := func(t *testing.T) { + performRegistration := func(t *testing.T) { f := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) loginOrRegister(t, f.ID, f.SessionTokenExchangeCode) } - login := func(t *testing.T) { + performLogin := func(t *testing.T) { f := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) loginOrRegister(t, f.ID, f.SessionTokenExchangeCode) } @@ -582,16 +582,16 @@ func TestStrategy(t *testing.T) { first, then func(*testing.T) }{{ name: "login-twice", - first: login, then: login, + first: performLogin, then: performLogin, }, { name: "login-then-register", - first: login, then: register, + first: performLogin, then: performRegistration, }, { name: "register-then-login", - first: register, then: login, + first: performRegistration, then: performLogin, }, { name: "register-twice", - first: register, then: register, + first: performRegistration, then: performRegistration, }} { t.Run("case="+tc.name, func(t *testing.T) { subject = tc.name + "-api-code-testing@ory.sh" @@ -599,6 +599,31 @@ func TestStrategy(t *testing.T) { tc.then(t) }) } + t.Run("case=should use redirect_to URL on failure", func(t *testing.T) { + ctx := context.Background() + subject = "existing-subject-api-code-testing@ory.sh" + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject}, + }) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + + f := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) + + _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: f.SessionTokenExchangeCode}) + require.Error(t, err) + + action := assertFormValues(t, f.ID, "valid") + returnToURL := makeAPICodeFlowRequest(t, "valid", action) + returnedFlow := returnToURL.Query().Get("flow") + + require.NotEmpty(t, returnedFlow, "flow query param was empty in the return_to URL") + loginFlow, err := reg.LoginFlowPersister().GetLoginFlow(ctx, uuid.FromStringOrNil(returnedFlow)) + require.NoError(t, err) + assert.Equal(t, text.ErrorValidationDuplicateCredentials, loginFlow.UI.Messages[0].ID) + }) }) t.Run("case=submit id_token during registration or login", func(t *testing.T) { From ec90929e1590f3169f5f04267ebb2a941d8c802e Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 25 Apr 2024 07:46:55 +0000 Subject: [PATCH 086/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From c435727c1e3c70c040b7fc7648ce621b136e5fc2 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 25 Apr 2024 14:35:40 +0200 Subject: [PATCH 087/262] fix: enum type of session expandables (#3895) --- session/handler.go | 13 ++++++------- spec/api.json | 8 ++++---- spec/swagger.json | 12 ++++++------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/session/handler.go b/session/handler.go index 73ac5c32ec1b..8953fb37c6c3 100644 --- a/session/handler.go +++ b/session/handler.go @@ -339,16 +339,16 @@ type listSessionsRequest struct { // // required: false // in: query - ExpandOptions []ListSessionExpandable `json:"expand"` + ExpandOptions []SessionExpandable `json:"expand"` } // Expandable properties of a session -// swagger:enum ListSessionExpandable -type ListSessionExpandable string +// swagger:enum SessionExpandable +type SessionExpandable string const ( - ListSessionExpandableIdentity ListSessionExpandable = "identity" - ListSessionExpandableDevices ListSessionExpandable = "devices" + SessionExpandableIdentity SessionExpandable = "identity" + SessionExpandableDevices SessionExpandable = "devices" ) // Session List Response @@ -440,9 +440,8 @@ type getSession struct { // If no value is provided, the expandable properties are skipped. // // required: false - // enum: identity,devices // in: query - ExpandOptions []string `json:"expand"` + ExpandOptions []SessionExpandable `json:"expand"` // ID is the session's ID. // diff --git a/spec/api.json b/spec/api.json index 5155b2e37554..f3ea406c37ec 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4901,11 +4901,11 @@ "in": "query", "name": "expand", "schema": { - "enum": [ - "identity", - "devices" - ], "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "type": "array" diff --git a/spec/swagger.json b/spec/swagger.json index fc38a382e4c8..dad8a7a36b25 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1090,12 +1090,12 @@ "operationId": "getSession", "parameters": [ { - "enum": [ - "identity", - "devices" - ], "type": "array", "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "description": "ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session.\nExample - ?expand=Identity\u0026expand=Devices\nIf no value is provided, the expandable properties are skipped.", @@ -3251,9 +3251,9 @@ "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, "NullTime": { - "description": "NullTime implements the Scanner interface so\nit can be used as a scan destination, similar to NullString.", + "description": "NullTime implements the [Scanner] interface so\nit can be used as a scan destination, similar to [NullString].", "type": "object", - "title": "NullTime represents a time.Time that may be null.", + "title": "NullTime represents a [time.Time] that may be null.", "properties": { "Time": { "type": "string", From da6b38a3d12e5d0083f3e229ae395ead81eed43f Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:37:23 +0000 Subject: [PATCH 088/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- spec/swagger.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/swagger.json b/spec/swagger.json index dad8a7a36b25..bf6546676192 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3251,9 +3251,9 @@ "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, "NullTime": { - "description": "NullTime implements the [Scanner] interface so\nit can be used as a scan destination, similar to [NullString].", + "description": "NullTime implements the Scanner interface so\nit can be used as a scan destination, similar to NullString.", "type": "object", - "title": "NullTime represents a [time.Time] that may be null.", + "title": "NullTime represents a time.Time that may be null.", "properties": { "Time": { "type": "string", From 41310b3dfe7dceb583f6fb16477d208e52ac2163 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:26:49 +0000 Subject: [PATCH 089/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ce844b9ba1..e1eb230106a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-04-22)](#2024-04-22) +- [ (2024-04-25)](#2024-04-25) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-22) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-25) ## Breaking Changes @@ -376,6 +376,12 @@ defaults to `false`. - Drop trigram index on identifiers ([#3827](https://github.com/ory/kratos/issues/3827)) ([8f8fd90](https://github.com/ory/kratos/commit/8f8fd90304886ecd689a85fc60c4712e47526cdd)) +- Enum type of session expandables + ([#3891](https://github.com/ory/kratos/issues/3891)) + ([63d785e](https://github.com/ory/kratos/commit/63d785e5e73ff067ec804ecc2107fac1525d3688)) +- Enum type of session expandables + ([#3895](https://github.com/ory/kratos/issues/3895)) + ([c435727](https://github.com/ory/kratos/commit/c435727c1e3c70c040b7fc7648ce621b136e5fc2)) - Execute verification & verification_ui properly in login flows ([#3847](https://github.com/ory/kratos/issues/3847)) ([5aad1c1](https://github.com/ory/kratos/commit/5aad1c1e6cc92f72af56511dacb9812edb600813)) @@ -402,6 +408,25 @@ defaults to `false`. - Prevent SMTP URL leak on unparsable URL ([#3770](https://github.com/ory/kratos/issues/3770)) ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) +- Respect return_to in OIDC API flow error case + ([#3893](https://github.com/ory/kratos/issues/3893)) + ([e8f1bcb](https://github.com/ory/kratos/commit/e8f1bcb1342af994b8e08282aa4066ee00ffe7d4)): + + - fix: respect return_to in OIDC API flow error case + + This fix ensures that we redirect the user to the return_to URL when an error + occurs during the OIDC login for native flows. + + Native flows are initialized through the API, and the browser URL is retrieved + from a 422 response after a POST to submit the login flow. Successful OIDC + flows already returned the `code` to the `return_to` URL. Now, unsuccessful + flows return the `flow` with the current flow ID (which might have changed), + so that the caller can retrieve the full flow and act accordingly. + + - fix: ignore trivvy CVE report + + Bump in distroless is still open + - **sdk:** Expand identity in session extension ([#3843](https://github.com/ory/kratos/issues/3843)) ([04f0231](https://github.com/ory/kratos/commit/04f02318d4de5290cbf100e9b301284d5ee40fe7)), From 9f34a21ea2035a5d33edd96753023a3c8c6c054c Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Fri, 26 Apr 2024 12:42:52 +0200 Subject: [PATCH 090/262] fix: db index and duplicate credentials error (#3896) * fix: don't return password cred type if empty * fix: better index for config.user_handle on identity_credentials --- identity/manager.go | 31 ++++++++++++++++++- identity/manager_test.go | 25 +++++++++++++++ internal/client-go/go.sum | 1 + .../sql/identity/persister_identity.go | 4 +-- ...s_fix_user_handle_index.cockroach.down.sql | 1 + ...als_fix_user_handle_index.cockroach.up.sql | 4 +++ ...credentials_fix_user_handle_index.down.sql | 0 ...y_credentials_fix_user_handle_index.up.sql | 0 ...s_fix_user_handle_index.cockroach.down.sql | 3 ++ ...als_fix_user_handle_index.cockroach.up.sql | 1 + ...credentials_fix_user_handle_index.down.sql | 0 ...y_credentials_fix_user_handle_index.up.sql | 0 selfservice/strategy/oidc/strategy_test.go | 2 +- .../passkey/passkey_registration_test.go | 2 +- .../strategy/webauthn/registration_test.go | 2 +- text/message_validation.go | 2 ++ 16 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.down.sql create mode 100644 persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.up.sql create mode 100644 persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.down.sql create mode 100644 persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.up.sql create mode 100644 persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.down.sql create mode 100644 persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.up.sql create mode 100644 persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.down.sql create mode 100644 persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.up.sql diff --git a/identity/manager.go b/identity/manager.go index c5ab32bbe293..04fb3edae500 100644 --- a/identity/manager.go +++ b/identity/manager.go @@ -205,8 +205,19 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi if len(cred.Identifiers) > 0 { identifierHint = cred.Identifiers[0] } - duplicateCredErr.AddCredentialsType(cred.Type) duplicateCredErr.SetIdentifierHint(identifierHint) + + var cfg CredentialsPassword + if err := json.Unmarshal(cred.Config, &cfg); err != nil { + // just ignore this credential if the config is invalid + continue + } + if cfg.HashedPassword == "" { + // just ignore this credential if the hashed password is empty + continue + } + + duplicateCredErr.AddCredentialsType(cred.Type) case CredentialsTypeOIDC: var cfg CredentialsOIDC if err := json.Unmarshal(cred.Config, &cfg); err != nil { @@ -232,6 +243,24 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi identifierHint = cred.Identifiers[0] } + for _, webauthn := range cfg.Credentials { + if webauthn.IsPasswordless { + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.SetIdentifierHint(identifierHint) + break + } + } + case CredentialsTypePasskey: + var cfg CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cfg); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to JSON decode identity credentials %s for identity %s.", cred.Type, found.ID)) + } + + identifierHint := foundConflictAddress + if len(cred.Identifiers) > 0 { + identifierHint = cred.Identifiers[0] + } + for _, webauthn := range cfg.Credentials { if webauthn.IsPasswordless { duplicateCredErr.AddCredentialsType(cred.Type) diff --git a/identity/manager_test.go b/identity/manager_test.go index 5eb3e61aa58c..e0346b8ee0c0 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -223,6 +223,31 @@ func TestManager(t *testing.T) { assert.Equal(t, verr.IdentifierHint(), email) }) + t.Run("type=oidc", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + creds := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + // Identifiers in OIDC are not email addresses, but a unique user ID. + Identifiers: []string{"google:" + uuid.Must(uuid.NewV4()).String()}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider": "google"},{"provider": "github"}]}`), + }, + } + + first := createIdentity(email, "email_creds", creds) + require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(context.Background(), second) + require.Error(t, err) + + var verr = new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.ElementsMatch(t, []string{"oidc"}, verr.AvailableCredentials()) + assert.ElementsMatch(t, []string{"google", "github"}, verr.AvailableOIDCProviders()) + assert.Equal(t, email, verr.IdentifierHint()) + }) + t.Run("type=password+oidc+webauthn", func(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" creds := map[identity.CredentialsType]identity.Credentials{ diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index ea532485944e..52daaf2fc71b 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -267,9 +267,9 @@ INNER JOIN identity_credentials FROM identity_credential_types WHERE name = ? ) -WHERE identity_credentials.config ->> '%s' = ? +WHERE identity_credentials.config ->> '%s' = ? AND identity_credentials.config ->> '%s' IS NOT NULL AND identities.nid = ? -LIMIT 1`, jsonPath), +LIMIT 1`, jsonPath, jsonPath), identity.CredentialsTypeWebAuthn, base64.StdEncoding.EncodeToString(userHandle), p.NetworkID(ctx), diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.down.sql new file mode 100644 index 000000000000..dd8f3d45ff55 --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credentials_config_user_handle_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.up.sql new file mode 100644 index 000000000000..a953dd44b8b1 --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.up.sql @@ -0,0 +1,4 @@ +CREATE INDEX identity_credentials_config_user_handle_idx + ON identity_credentials ((config ->> 'user_handle')) + WHERE config ->> 'user_handle' IS NOT NULL +; diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.down.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.up.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.down.sql new file mode 100644 index 000000000000..14c295e29c5d --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.down.sql @@ -0,0 +1,3 @@ +CREATE INVERTED INDEX identity_credentials_user_handle_idx + ON identity_credentials (config) + WHERE config ->> 'user_handle' IS NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.up.sql new file mode 100644 index 000000000000..91e0c2a6c2c2 --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX identity_credentials_user_handle_idx; diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.down.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.up.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 0b6d068b21fc..b02e4fc45853 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -1285,7 +1285,7 @@ func TestStrategy(t *testing.T) { var linkingLoginFlow struct{ ID string } t.Run("step=should fail login and start a new login", func(t *testing.T) { res, body := loginWithOIDC(t, client, loginFlow.ID, "valid") - assertUIError(t, res, body, "You tried signing in with existing-oidc-identity-1@ory.sh which is already in use by another account. You can sign in using social sign in, or your password. You can sign in using one of the following social sign in providers: Secondprovider.") + assertUIError(t, res, body, "You tried signing in with existing-oidc-identity-1@ory.sh which is already in use by another account. You can sign in using social sign in. You can sign in using one of the following social sign in providers: Secondprovider.") linkingLoginFlow.ID = gjson.GetBytes(body, "id").String() assert.NotEqual(t, loginFlow.ID.String(), linkingLoginFlow.ID, "should have started a new flow") }) diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go index 1a4759dfa09e..d495e8c4dfe4 100644 --- a/selfservice/strategy/passkey/passkey_registration_test.go +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -372,7 +372,7 @@ func TestRegistration(t *testing.T) { assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username") assert.Equal(t, - "You tried signing in with "+email+" which is already in use by another account. You can sign in using your password.", + "You tried signing in with "+email+" which is already in use by another account. You can sign in using your passkey.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) }) } diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index c65a2c1ec030..c0503b151ed8 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -438,7 +438,7 @@ func TestRegistration(t *testing.T) { actual, _, _ = makeRegistration(t, f, values(email)) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username") - assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your password, or your passkey or a security key.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your passkey or a security key.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) }) } }) diff --git a/text/message_validation.go b/text/message_validation.go index b3dff02c3234..c10fddead805 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -286,6 +286,8 @@ func NewErrorValidationDuplicateCredentialsWithHints(availableCredentialTypes [] humanReadable = append(humanReadable, "social sign in") case "webauthn": humanReadable = append(humanReadable, "your passkey or a security key") + case "passkey": + humanReadable = append(humanReadable, "your passkey") } } if len(humanReadable) == 0 { From ab8e1b5ba7b40efbb87e8ae7df848af69102f70e Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:44:16 +0000 Subject: [PATCH 091/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From cc39f8df7c235af0df616432bc4f88681896ad85 Mon Sep 17 00:00:00 2001 From: guangwu Date: Fri, 26 Apr 2024 22:18:05 +0800 Subject: [PATCH 092/262] fix: close res body (#3870) Signed-off-by: guoguangwu --- internal/testhelpers/selfservice_verification.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/testhelpers/selfservice_verification.go b/internal/testhelpers/selfservice_verification.go index 25c40bf32f5e..9786c8210e73 100644 --- a/internal/testhelpers/selfservice_verification.go +++ b/internal/testhelpers/selfservice_verification.go @@ -84,6 +84,7 @@ func GetRecoveryFlowForType(t *testing.T, client *http.Client, ts *httptest.Serv res, err := client.Get(url) require.NoError(t, err) + defer res.Body.Close() var flowID string switch ft { From 264395a54b46da625711cdfb0f13d50ba97f3f92 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:10:10 +0000 Subject: [PATCH 093/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1eb230106a2..e9a081e3be7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-04-25)](#2024-04-25) +- [ (2024-04-26)](#2024-04-26) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Features](#features) @@ -322,7 +322,7 @@ -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-25) +# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-26) ## Breaking Changes @@ -362,6 +362,15 @@ defaults to `false`. - Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) +- Close res body ([#3870](https://github.com/ory/kratos/issues/3870)) + ([cc39f8d](https://github.com/ory/kratos/commit/cc39f8df7c235af0df616432bc4f88681896ad85)) +- Db index and duplicate credentials error + ([#3896](https://github.com/ory/kratos/issues/3896)) + ([9f34a21](https://github.com/ory/kratos/commit/9f34a21ea2035a5d33edd96753023a3c8c6c054c)): + + - fix: don't return password cred type if empty + - fix: better index for config.user_handle on identity_credentials + - Do not require method to be passkey in settings schema ([#3862](https://github.com/ory/kratos/issues/3862)) ([660f330](https://github.com/ory/kratos/commit/660f330ab69ef0e6fd21501fbc9dfed693d4a715)) From 3ecdf2bfec9e5460b44cf7eeacba532b409a19c0 Mon Sep 17 00:00:00 2001 From: camcui <166618273+camcui@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:45:35 +0800 Subject: [PATCH 094/262] chore: fix function name in comment (#3869) Signed-off-by: camcui Co-authored-by: Jonas Hungershausen --- selfservice/hook/show_verification_ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfservice/hook/show_verification_ui.go b/selfservice/hook/show_verification_ui.go index 566546a028da..65a5935ec7a6 100644 --- a/selfservice/hook/show_verification_ui.go +++ b/selfservice/hook/show_verification_ui.go @@ -50,7 +50,7 @@ func (e *ShowVerificationUIHook) ExecutePostRegistrationPostPersistHook(_ http.R }) } -// ExecutePostRegistrationPostPersistHook adds redirect headers and status code if the request is a browser request. +// ExecuteLoginPostHook adds redirect headers and status code if the request is a browser request. // If the request is not a browser request, this hook does nothing. func (e *ShowVerificationUIHook) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, _ *session.Session) error { return otelx.WithSpan(r.Context(), "selfservice.hook.ShowVerificationUIHook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { From cd01cb9fb23a24e52d46538a9ea63c2144c3b145 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 2 May 2024 10:56:05 +0200 Subject: [PATCH 095/262] docs: remove delete reference from batch patch identity (#3906) --- identity/handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index 00f4a011e8d0..3ac641e09377 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -550,9 +550,9 @@ func (h *Handler) identityFromCreateIdentityBody(ctx context.Context, cr *Create // swagger:route PATCH /admin/identities identity batchPatchIdentities // -// # Create and deletes multiple identities +// # Create multiple identities // -// Creates or delete multiple +// Creates multiple // [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). // This endpoint can also be used to [import // credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) From 644e669116c6bfb92d128df6104d93059dc28d77 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 2 May 2024 08:57:29 +0000 Subject: [PATCH 096/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/README.md | 2 +- internal/client-go/api_identity.go | 8 ++++---- internal/httpclient/README.md | 2 +- internal/httpclient/api_identity.go | 8 ++++---- spec/api.json | 4 ++-- spec/swagger.json | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/client-go/README.md b/internal/client-go/README.md index 33082914bfef..04dd61ab7d1e 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -111,7 +111,7 @@ Class | Method | HTTP request | Description *FrontendApi* | [**UpdateRegistrationFlow**](docs/FrontendApi.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow *FrontendApi* | [**UpdateSettingsFlow**](docs/FrontendApi.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow *FrontendApi* | [**UpdateVerificationFlow**](docs/FrontendApi.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow -*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create and deletes multiple identities +*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create multiple identities *IdentityApi* | [**CreateIdentity**](docs/IdentityApi.md#createidentity) | **Post** /admin/identities | Create an Identity *IdentityApi* | [**CreateRecoveryCodeForIdentity**](docs/IdentityApi.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code *IdentityApi* | [**CreateRecoveryLinkForIdentity**](docs/IdentityApi.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index c898733ecd4b..04774a5b3938 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -29,8 +29,8 @@ var ( type IdentityApi interface { /* - * BatchPatchIdentities Create and deletes multiple identities - * Creates or delete multiple + * BatchPatchIdentities Create multiple identities + * Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) @@ -327,8 +327,8 @@ func (r IdentityApiApiBatchPatchIdentitiesRequest) Execute() (*BatchPatchIdentit } /* - - BatchPatchIdentities Create and deletes multiple identities - - Creates or delete multiple + - BatchPatchIdentities Create multiple identities + - Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 33082914bfef..04dd61ab7d1e 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -111,7 +111,7 @@ Class | Method | HTTP request | Description *FrontendApi* | [**UpdateRegistrationFlow**](docs/FrontendApi.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow *FrontendApi* | [**UpdateSettingsFlow**](docs/FrontendApi.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow *FrontendApi* | [**UpdateVerificationFlow**](docs/FrontendApi.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow -*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create and deletes multiple identities +*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create multiple identities *IdentityApi* | [**CreateIdentity**](docs/IdentityApi.md#createidentity) | **Post** /admin/identities | Create an Identity *IdentityApi* | [**CreateRecoveryCodeForIdentity**](docs/IdentityApi.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code *IdentityApi* | [**CreateRecoveryLinkForIdentity**](docs/IdentityApi.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index c898733ecd4b..04774a5b3938 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -29,8 +29,8 @@ var ( type IdentityApi interface { /* - * BatchPatchIdentities Create and deletes multiple identities - * Creates or delete multiple + * BatchPatchIdentities Create multiple identities + * Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) @@ -327,8 +327,8 @@ func (r IdentityApiApiBatchPatchIdentitiesRequest) Execute() (*BatchPatchIdentit } /* - - BatchPatchIdentities Create and deletes multiple identities - - Creates or delete multiple + - BatchPatchIdentities Create multiple identities + - Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import diff --git a/spec/api.json b/spec/api.json index f3ea406c37ec..48b0d934d382 100644 --- a/spec/api.json +++ b/spec/api.json @@ -3918,7 +3918,7 @@ ] }, "patch": { - "description": "Creates or delete multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", + "description": "Creates multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", "operationId": "batchPatchIdentities", "requestBody": { "content": { @@ -3977,7 +3977,7 @@ "oryAccessToken": [] } ], - "summary": "Create and deletes multiple identities", + "summary": "Create multiple identities", "tags": [ "identity" ] diff --git a/spec/swagger.json b/spec/swagger.json index bf6546676192..65bb84f4e41e 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -339,7 +339,7 @@ "oryAccessToken": [] } ], - "description": "Creates or delete multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", + "description": "Creates multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", "consumes": [ "application/json" ], @@ -353,7 +353,7 @@ "tags": [ "identity" ], - "summary": "Create and deletes multiple identities", + "summary": "Create multiple identities", "operationId": "batchPatchIdentities", "parameters": [ { From e5d3b0afde3c80c6c9cf8815c56d82e291ede663 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Thu, 2 May 2024 10:58:15 +0200 Subject: [PATCH 097/262] fix: CVEs in dependencies (#3902) --- .docker/Dockerfile-build | 3 +-- .docker/Dockerfile-debug | 2 +- .grype.yaml | 1 + go.mod | 10 +++++----- go.sum | 16 ++++++++++------ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build index 6ee16085cf84..95993b490f66 100644 --- a/.docker/Dockerfile-build +++ b/.docker/Dockerfile-build @@ -1,6 +1,5 @@ # syntax = docker/dockerfile:1-experimental -# Workaround for https://github.com/GoogleContainerTools/distroless/issues/1342 -FROM golang:1.21 AS builder +FROM golang:1.21-bullseye AS builder RUN apt-get update && apt-get upgrade -y &&\ mkdir -p /var/lib/sqlite diff --git a/.docker/Dockerfile-debug b/.docker/Dockerfile-debug index 9ed036daebe7..ebebf9d84058 100644 --- a/.docker/Dockerfile-debug +++ b/.docker/Dockerfile-debug @@ -1,4 +1,4 @@ -FROM golang:1.21 +FROM golang:1.21-bullseye ENV CGO_ENABLED 1 RUN apt-get update && apt-get install -y --no-install-recommends inotify-tools psmisc diff --git a/.grype.yaml b/.grype.yaml index 1ba341fccad5..57438622ad00 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -1,5 +1,6 @@ #only-fixed: true ignore: + - vulnerability: GHSA-c5pj-mqfh-rvc3 # https://github.com/advisories/GHSA-c5pj-mqfh-rvc3 - vulnerability: CVE-2015-5237 - vulnerability: CVE-2022-30065 - vulnerability: CVE-2023-2650 diff --git a/go.mod b/go.mod index 655a07bbf9e2..8f8d3c1e60ec 100644 --- a/go.mod +++ b/go.mod @@ -99,9 +99,9 @@ require ( go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/sdk v1.21.0 go.opentelemetry.io/otel/trace v1.22.0 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.22.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/net v0.21.0 + golang.org/x/net v0.24.0 golang.org/x/oauth2 v0.16.0 golang.org/x/sync v0.5.0 golang.org/x/text v0.14.0 @@ -135,7 +135,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/docker/cli v20.10.21+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v20.10.24+incompatible // indirect + github.com/docker/docker v20.10.27+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect @@ -308,8 +308,8 @@ require ( go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/tools v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index f344856176b4..964e696c2c4b 100644 --- a/go.sum +++ b/go.sum @@ -160,8 +160,8 @@ github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SH github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= -github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.27+incompatible h1:Id/ZooynV4ZlD6xX20RCd3SR0Ikn7r4QZDa2ECK2TgA= +github.com/docker/docker v20.10.27+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -1116,8 +1116,9 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1212,8 +1213,9 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.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 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1328,8 +1330,9 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1342,8 +1345,9 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 5288bc701f4f2c271c88ca7f87b79a8d88beb7da Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Thu, 2 May 2024 19:03:02 +0200 Subject: [PATCH 098/262] chore: make identity schema provider a proper service (#3908) --- .docker/Dockerfile-build | 2 +- .docker/Dockerfile-debug | 2 +- .github/workflows/ci.yaml | 6 +- .github/workflows/format.yml | 2 +- .github/workflows/licenses.yml | 2 +- driver/registry.go | 27 +++++--- driver/registry_default.go | 12 +++- driver/registry_default_schemas.go | 27 ++------ driver/registry_default_schemas_test.go | 2 +- embedx/config.schema.json | 13 ++++ identity/validator.go | 2 +- internal/client-go/go.sum | 1 + .../sql/identity/persister_identity.go | 2 +- persistence/sql/persister.go | 2 +- persistence/sql/persister_hmac_test.go | 2 +- schema/handler.go | 2 +- schema/schema.go | 66 ++++++++++++++++--- selfservice/flow/settings/error.go | 3 +- selfservice/flow/settings/handler.go | 2 +- selfservice/strategy/code/strategy.go | 2 +- selfservice/strategy/link/strategy.go | 2 +- selfservice/strategy/profile/strategy.go | 8 +-- 22 files changed, 122 insertions(+), 67 deletions(-) diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build index 95993b490f66..bd619930f0a9 100644 --- a/.docker/Dockerfile-build +++ b/.docker/Dockerfile-build @@ -1,5 +1,5 @@ # syntax = docker/dockerfile:1-experimental -FROM golang:1.21-bullseye AS builder +FROM golang:1.22-bullseye AS builder RUN apt-get update && apt-get upgrade -y &&\ mkdir -p /var/lib/sqlite diff --git a/.docker/Dockerfile-debug b/.docker/Dockerfile-debug index ebebf9d84058..a309b5ad92bb 100644 --- a/.docker/Dockerfile-debug +++ b/.docker/Dockerfile-debug @@ -1,4 +1,4 @@ -FROM golang:1.21-bullseye +FROM golang:1.22-bullseye ENV CGO_ENABLED 1 RUN apt-get update && apt-get install -y --no-install-recommends inotify-tools psmisc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6d4ed3dc155b..2d4e28d070dc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -79,7 +79,7 @@ jobs: fetch-depth: 2 - uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.22" - run: go list -json > go.list - name: Run nancy uses: sonatype-nexus-community/nancy-github-action@v1.0.2 @@ -170,7 +170,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.22" - name: Install selfservice-ui-react-native uses: actions/checkout@v3 @@ -274,7 +274,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.22" - run: go build -tags sqlite,json1 . - name: Install selfservice-ui-react-native diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index ce6695943cd8..7e243923b8ca 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: "1.21" + go-version: "1.22" - run: make format - name: Indicate formatting issues run: git diff HEAD --exit-code --color diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml index 8871ccb2c542..8a86486031de 100644 --- a/.github/workflows/licenses.yml +++ b/.github/workflows/licenses.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: "1.21" + go-version: "1.22" - uses: actions/setup-node@v2 with: node-version: "18" diff --git a/driver/registry.go b/driver/registry.go index e87fdd076078..dc31f7305633 100644 --- a/driver/registry.go +++ b/driver/registry.go @@ -106,7 +106,7 @@ type Registry interface { courier.PersistenceProvider schema.HandlerProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider password2.ValidationProvider @@ -180,15 +180,16 @@ func NewRegistryFromDSN(ctx context.Context, c *config.Config, l *logrusx.Logger } type options struct { - skipNetworkInit bool - config *config.Config - replaceTracer func(*otelx.Tracer) *otelx.Tracer - inspect func(Registry) error - extraMigrations []fs.FS - replacementStrategies []NewStrategy - extraHooks map[string]func(config.SelfServiceHook) any - disableMigrationLogging bool - jsonnetPool jsonnetsecure.Pool + skipNetworkInit bool + config *config.Config + replaceTracer func(*otelx.Tracer) *otelx.Tracer + replaceIdentitySchemaProvider func(Registry) schema.IdentitySchemaProvider + inspect func(Registry) error + extraMigrations []fs.FS + replacementStrategies []NewStrategy + extraHooks map[string]func(config.SelfServiceHook) any + disableMigrationLogging bool + jsonnetPool jsonnetsecure.Pool } type RegistryOption func(*options) @@ -209,6 +210,12 @@ func WithConfig(config *config.Config) RegistryOption { } } +func WithIdentitySchemaProvider(f func(r Registry) schema.IdentitySchemaProvider) RegistryOption { + return func(o *options) { + o.replaceIdentitySchemaProvider = f + } +} + func ReplaceTracer(f func(*otelx.Tracer) *otelx.Tracer) RegistryOption { return func(o *options) { o.replaceTracer = f diff --git a/driver/registry_default.go b/driver/registry_default.go index 1ab63c0af561..eab63a120981 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -93,9 +93,10 @@ type RegistryDefault struct { hookCodeAddressVerifier *hook.CodeAddressVerifier hookTwoStepRegistration *hook.TwoStepRegistration - identityHandler *identity.Handler - identityValidator *identity.Validator - identityManager *identity.Manager + identityHandler *identity.Handler + identityValidator *identity.Validator + identityManager *identity.Manager + identitySchemaProvider schema.IdentitySchemaProvider courierHandler *courier.Handler @@ -621,6 +622,7 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize instrumentedsql.WithOmitArgs(), // don't risk leaking PII or secrets } } + if o.replaceTracer != nil { m.trc = o.replaceTracer(m.trc) } @@ -633,6 +635,10 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize m.WithHooks(o.extraHooks) } + if o.replaceIdentitySchemaProvider != nil { + m.identitySchemaProvider = o.replaceIdentitySchemaProvider(m) + } + bc := backoff.NewExponentialBackOff() bc.MaxElapsedTime = time.Minute * 5 bc.Reset() diff --git a/driver/registry_default_schemas.go b/driver/registry_default_schemas.go index 9f68fbd2d86e..d3d61a3e7e35 100644 --- a/driver/registry_default_schemas.go +++ b/driver/registry_default_schemas.go @@ -5,32 +5,13 @@ package driver import ( "context" - "net/url" - - "github.com/pkg/errors" "github.com/ory/kratos/schema" ) -func (m *RegistryDefault) IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) { - ms, err := m.Config().IdentityTraitsSchemas(ctx) - if err != nil { - return nil, err +func (m *RegistryDefault) IdentityTraitsSchemas(ctx context.Context) (schema.IdentitySchemaList, error) { + if m.identitySchemaProvider == nil { + m.identitySchemaProvider = schema.NewDefaultIdentityTraitsProvider(m) } - - var ss schema.Schemas - for _, s := range ms { - surl, err := url.Parse(s.URL) - if err != nil { - return nil, errors.WithStack(err) - } - - ss = append(ss, schema.Schema{ - ID: s.ID, - URL: surl, - RawURL: s.URL, - }) - } - - return ss, nil + return m.identitySchemaProvider.IdentityTraitsSchemas(ctx) } diff --git a/driver/registry_default_schemas_test.go b/driver/registry_default_schemas_test.go index 8d76c22bf491..aed6819a383b 100644 --- a/driver/registry_default_schemas_test.go +++ b/driver/registry_default_schemas_test.go @@ -39,7 +39,7 @@ func TestRegistryDefault_IdentityTraitsSchemas(t *testing.T) { ss, err := reg.IdentityTraitsSchemas(context.Background()) require.NoError(t, err) - assert.Equal(t, 2, len(ss)) + assert.Equal(t, 2, ss.Total()) assert.Contains(t, ss, defaultSchema) assert.Contains(t, ss, altSchema) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 0a94c54dbb23..79bc83c88b1d 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2861,6 +2861,19 @@ "description": "Secifies which organizations are available. Only effective in the Ory Network.", "type": "array", "default": [] + }, + "enterprise": { + "title": "Enterprise features", + "description": "Specifies enterprise features. Only effective in the Ory Network or with a valid license.", + "type": "object", + "properties": { + "identity_schema_fallback_url_template": { + "type": "string", + "title": "Fallback URL template for identity schemas", + "description": "A fallback URL template used when looking up identity schemas." + } + }, + "additionalProperties": false } }, "allOf": [ diff --git a/identity/validator.go b/identity/validator.go index 3bd8f9476fa5..b977105f49bd 100644 --- a/identity/validator.go +++ b/identity/validator.go @@ -19,7 +19,7 @@ import ( type ( validatorDependencies interface { - IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) + schema.IdentitySchemaProvider config.Provider } Validator struct { diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index 52daaf2fc71b..984fb0199da2 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -47,7 +47,7 @@ var ( ) type dependencies interface { - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider identity.ValidationProvider x.LoggingProvider config.Provider diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go index 99990b7ace91..85bcdf7466c8 100644 --- a/persistence/sql/persister.go +++ b/persistence/sql/persister.go @@ -40,7 +40,7 @@ type ( config.Provider contextx.Provider x.TracingProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider identity.ValidationProvider } Persister struct { diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index 05dcd5985908..c7adcdce3a1e 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -52,7 +52,7 @@ func (l *logRegistryOnly) Audit() *logrusx.Logger { func (l *logRegistryOnly) Tracer(ctx context.Context) *otelx.Tracer { return otelx.NewNoop(l.l, new(otelx.Config)) } -func (l *logRegistryOnly) IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) { +func (l *logRegistryOnly) IdentityTraitsSchemas(ctx context.Context) (schema.IdentitySchemaList, error) { panic("implement me") } diff --git a/schema/handler.go b/schema/handler.go index e154ae75d699..ff06bc8c43ef 100644 --- a/schema/handler.go +++ b/schema/handler.go @@ -31,7 +31,7 @@ type ( handlerDependencies interface { x.WriterProvider x.LoggingProvider - IdentityTraitsProvider + IdentitySchemaProvider x.CSRFProvider config.Provider x.TracingProvider diff --git a/schema/schema.go b/schema/schema.go index 69b6bbca7332..40671c94a000 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -4,6 +4,7 @@ package schema import ( + "cmp" "context" "encoding/base64" "io" @@ -21,16 +22,58 @@ import ( "github.com/ory/x/urlx" ) +var _ IdentitySchemaList = (*Schemas)(nil) + type Schemas []Schema -type IdentityTraitsProvider interface { - IdentityTraitsSchemas(ctx context.Context) (Schemas, error) + +type IdentitySchemaProvider interface { + IdentityTraitsSchemas(ctx context.Context) (IdentitySchemaList, error) } -func (s Schemas) GetByID(id string) (*Schema, error) { - if id == "" { - id = config.DefaultIdentityTraitsSchemaID +type deps interface { + config.Provider +} + +type DefaultIdentitySchemaProvider struct { + d deps +} + +func NewDefaultIdentityTraitsProvider(d deps) *DefaultIdentitySchemaProvider { + return &DefaultIdentitySchemaProvider{d: d} +} + +func (d *DefaultIdentitySchemaProvider) IdentityTraitsSchemas(ctx context.Context) (IdentitySchemaList, error) { + ms, err := d.d.Config().IdentityTraitsSchemas(ctx) + if err != nil { + return nil, err + } + + var ss Schemas + for _, s := range ms { + surl, err := url.Parse(s.URL) + if err != nil { + return nil, errors.WithStack(err) + } + + ss = append(ss, Schema{ + ID: s.ID, + URL: surl, + RawURL: s.URL, + }) } + return ss, nil +} + +type IdentitySchemaList interface { + GetByID(id string) (*Schema, error) + Total() int + List(page, perPage int) Schemas +} + +func (s Schemas) GetByID(id string) (*Schema, error) { + id = cmp.Or(id, config.DefaultIdentityTraitsSchemaID) + for _, ss := range s { if ss.ID == id { return &ss, nil @@ -98,11 +141,16 @@ func GetKeysInOrder(ctx context.Context, schemaRef string) ([]string, error) { } type Schema struct { - ID string `json:"id"` - URL *url.URL `json:"-"` - RawURL string `json:"url"` + ID string `json:"id"` + URL *url.URL `json:"-"` + // RawURL contains the raw URL value as it was passed in the configuration. URL parsing can break base64 encoded URLs. + RawURL string `json:"url"` } func (s *Schema) SchemaURL(host *url.URL) *url.URL { - return urlx.AppendPaths(host, SchemasPath, base64.RawURLEncoding.EncodeToString([]byte(s.ID))) + return IDToURL(host, s.ID) +} + +func IDToURL(host *url.URL, id string) *url.URL { + return urlx.AppendPaths(host, SchemasPath, base64.RawURLEncoding.EncodeToString([]byte(id))) } diff --git a/selfservice/flow/settings/error.go b/selfservice/flow/settings/error.go index 2fd190c888e9..2583cd9dd526 100644 --- a/selfservice/flow/settings/error.go +++ b/selfservice/flow/settings/error.go @@ -4,7 +4,6 @@ package settings import ( - "context" "net/http" "net/url" @@ -43,7 +42,7 @@ type ( HandlerProvider FlowPersistenceProvider - IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) + schema.IdentitySchemaProvider } ErrorHandlerProvider interface{ SettingsFlowErrorHandler() *ErrorHandler } diff --git a/selfservice/flow/settings/handler.go b/selfservice/flow/settings/handler.go index 8b96ce85369a..efc1e5a84bc3 100644 --- a/selfservice/flow/settings/handler.go +++ b/selfservice/flow/settings/handler.go @@ -68,7 +68,7 @@ type ( HookExecutorProvider x.CSRFTokenGeneratorProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider login.HandlerProvider } diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index fd8993447744..ee3ce353e4ae 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -103,7 +103,7 @@ type ( RegistrationCodePersistenceProvider LoginCodePersistenceProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider session.PersistenceProvider sessiontokenexchange.PersistenceProvider diff --git a/selfservice/strategy/link/strategy.go b/selfservice/strategy/link/strategy.go index 8188b7e9e873..cdf8356cc4b3 100644 --- a/selfservice/strategy/link/strategy.go +++ b/selfservice/strategy/link/strategy.go @@ -75,7 +75,7 @@ type ( VerificationTokenPersistenceProvider SenderProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider } Strategy struct { diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go index 0942d738840e..b83e6ad527b2 100644 --- a/selfservice/strategy/profile/strategy.go +++ b/selfservice/strategy/profile/strategy.go @@ -62,7 +62,7 @@ type ( registration.FlowPersistenceProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider } Strategy struct { d strategyDependencies @@ -81,19 +81,19 @@ func (s *Strategy) SettingsStrategyID() string { func (s *Strategy) RegisterSettingsRoutes(public *x.RouterPublic) {} func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity, f *settings.Flow) error { - schemas, err := s.d.Config().IdentityTraitsSchemas(r.Context()) + schemas, err := s.d.IdentityTraitsSchemas(r.Context()) if err != nil { return err } - traitsSchema, err := schemas.FindSchemaByID(id.SchemaID) + traitsSchema, err := schemas.GetByID(id.SchemaID) if err != nil { return err } // use a schema compiler that disables identifiers schemaCompiler := jsonschema.NewCompiler() - nodes, err := container.NodesFromJSONSchema(r.Context(), node.ProfileGroup, traitsSchema.URL, "", schemaCompiler) + nodes, err := container.NodesFromJSONSchema(r.Context(), node.ProfileGroup, traitsSchema.URL.String(), "", schemaCompiler) if err != nil { return err } From d9dbaadc3088e6599cffaa358fcc5eab7e9cb2fb Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 2 May 2024 17:04:29 +0000 Subject: [PATCH 099/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - spec/swagger.json | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/spec/swagger.json b/spec/swagger.json index 65bb84f4e41e..1d548df9a00f 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3251,9 +3251,9 @@ "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, "NullTime": { - "description": "NullTime implements the Scanner interface so\nit can be used as a scan destination, similar to NullString.", + "description": "NullTime implements the [Scanner] interface so\nit can be used as a scan destination, similar to [NullString].", "type": "object", - "title": "NullTime represents a time.Time that may be null.", + "title": "NullTime represents a [time.Time] that may be null.", "properties": { "Time": { "type": "string", From 1a9a096d619925dd3718ad9dd9daf77387572ece Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Fri, 17 May 2024 14:36:26 +0200 Subject: [PATCH 100/262] fix(oidc): grace period for continuity container on oidc callbacks (#3915) --- continuity/manager.go | 27 +++--- continuity/manager_cookie.go | 24 ++++- continuity/manager_options_test.go | 2 +- continuity/manager_test.go | 45 +++++++++ continuity/persistence.go | 1 + continuity/test/persistence.go | 24 +++++ internal/client-go/go.sum | 1 + persistence/sql/persister_continuity.go | 17 ++++ selfservice/strategy/oidc/strategy.go | 7 +- selfservice/strategy/oidc/strategy_test.go | 102 +++++++++++++++++++++ x/cookie.go | 12 +++ 11 files changed, 241 insertions(+), 21 deletions(-) diff --git a/continuity/manager.go b/continuity/manager.go index 65989704faa0..779b0ae5f185 100644 --- a/continuity/manager.go +++ b/continuity/manager.go @@ -27,19 +27,18 @@ type Manager interface { } type managerOptions struct { - iid uuid.UUID - ttl time.Duration - payload json.RawMessage - payloadRaw interface{} - cleanUp bool + iid uuid.UUID + ttl time.Duration + setExpiresIn time.Duration + payload json.RawMessage + payloadRaw interface{} } type ManagerOption func(*managerOptions) error func newManagerOptions(opts []ManagerOption) (*managerOptions, error) { var o = &managerOptions{ - ttl: time.Minute, - cleanUp: true, + ttl: time.Minute * 10, } for _, opt := range opts { if err := opt(o); err != nil { @@ -49,13 +48,6 @@ func newManagerOptions(opts []ManagerOption) (*managerOptions, error) { return o, nil } -func DontCleanUp() ManagerOption { - return func(o *managerOptions) error { - o.cleanUp = false - return nil - } -} - func WithIdentity(i *identity.Identity) ManagerOption { return func(o *managerOptions) error { if i != nil { @@ -83,3 +75,10 @@ func WithPayload(payload interface{}) ManagerOption { return nil } } + +func WithExpireInsteadOfDelete(duration time.Duration) ManagerOption { + return func(o *managerOptions) error { + o.setExpiresIn = duration + return nil + } +} diff --git a/continuity/manager_cookie.go b/continuity/manager_cookie.go index 495800a87736..7d9b40632df5 100644 --- a/continuity/manager_cookie.go +++ b/continuity/manager_cookie.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "net/http" + "time" "github.com/gofrs/uuid" "github.com/pkg/errors" @@ -93,12 +94,22 @@ func (m *ManagerCookie) Continue(ctx context.Context, w http.ResponseWriter, r * } } - if err := x.SessionUnsetKey(w, r, m.d.ContinuityCookieManager(ctx), CookieName, name); err != nil { - return nil, err - } + if o.setExpiresIn > 0 { + if err := m.d.ContinuityPersister().SetContinuitySessionExpiry( + ctx, + container.ID, + time.Now().UTC().Add(o.setExpiresIn).Truncate(time.Second), + ); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { + return nil, err + } + } else { + if err := x.SessionUnsetKey(w, r, m.d.ContinuityCookieManager(ctx), CookieName, name); err != nil { + return nil, err + } - if err := m.d.ContinuityPersister().DeleteContinuitySession(ctx, container.ID); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { - return nil, err + if err := m.d.ContinuityPersister().DeleteContinuitySession(ctx, container.ID); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { + return nil, err + } } return container, nil @@ -136,6 +147,9 @@ func (m *ManagerCookie) container(ctx context.Context, w http.ResponseWriter, r return nil, errors.WithStack(ErrNotResumable.WithDebugf("Resumable ID from cookie could not be found in the datastore: %+v", err)) } else if err != nil { return nil, err + } else if container.ExpiresAt.Before(time.Now()) { + _ = x.SessionUnsetKey(w, r, m.d.ContinuityCookieManager(ctx), CookieName, name) + return nil, errors.WithStack(ErrNotResumable.WithDebugf("Resumable session has expired")) } return container, err diff --git a/continuity/manager_options_test.go b/continuity/manager_options_test.go index be2e9f73d4d0..8286f12e9e77 100644 --- a/continuity/manager_options_test.go +++ b/continuity/manager_options_test.go @@ -20,7 +20,7 @@ func TestManagerOptions(t *testing.T) { }{ { e: func(t *testing.T, actual *managerOptions) { - assert.EqualValues(t, time.Minute, actual.ttl) + assert.EqualValues(t, time.Minute*10, actual.ttl) }, }, { diff --git a/continuity/manager_test.go b/continuity/manager_test.go index 6790137faa80..8e71024d16cd 100644 --- a/continuity/manager_test.go +++ b/continuity/manager_test.go @@ -12,6 +12,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/ory/kratos/driver/config" @@ -181,6 +182,50 @@ func TestManager(t *testing.T) { assert.Contains(t, href, gjson.GetBytes(body, "name").String(), "%s", body) }) + t.Run("case=pause and use session with expiry", func(t *testing.T) { + cl := newClient() + + tc := &persisterTestCase{ + ro: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"}), continuity.WithExpireInsteadOfDelete(time.Minute)}, + wo: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{}), continuity.WithExpireInsteadOfDelete(time.Minute)}, + } + ts := newServer(t, p, tc) + genid := func() string { + return ts.URL + "/" + x.NewUUID().String() + } + + href := genid() + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + tc.ro = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"}), continuity.WithExpireInsteadOfDelete(-time.Minute)} + tc.wo = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{""}), continuity.WithExpireInsteadOfDelete(-time.Minute)} + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + body := ioutilx.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable.ReasonField) + }) + for k, tc := range []persisterTestCase{ {}, { diff --git a/continuity/persistence.go b/continuity/persistence.go index 2499abe59e21..cc912731fa87 100644 --- a/continuity/persistence.go +++ b/continuity/persistence.go @@ -18,5 +18,6 @@ type Persister interface { SaveContinuitySession(ctx context.Context, c *Container) error GetContinuitySession(ctx context.Context, id uuid.UUID) (*Container, error) DeleteContinuitySession(ctx context.Context, id uuid.UUID) error + SetContinuitySessionExpiry(ctx context.Context, id uuid.UUID, expiresAt time.Time) error DeleteExpiredContinuitySessions(ctx context.Context, deleteOlder time.Time, pageSize int) error } diff --git a/continuity/test/persistence.go b/continuity/test/persistence.go index cd6f61188c22..752ccca4d542 100644 --- a/continuity/test/persistence.go +++ b/continuity/test/persistence.go @@ -101,6 +101,30 @@ func TestPersister(ctx context.Context, p interface { }) }) + t.Run("case=set expiry", func(t *testing.T) { + // Create a new continuity session + expected := createContainer(t) + require.NoError(t, p.SaveContinuitySession(ctx, &expected)) + + // Set the expiry of the continuity session + newExpiry := time.Now().Add(48 * time.Hour).UTC().Truncate(time.Second) + require.NoError(t, p.SetContinuitySessionExpiry(ctx, expected.ID, newExpiry)) + + // Retrieve the continuity session + actual, err := p.GetContinuitySession(ctx, expected.ID) + require.NoError(t, err) + + // Check if the expiry has been updated + assert.EqualValues(t, newExpiry, actual.ExpiresAt) + + t.Run("can not update on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + newExpiry := time.Now().Add(12 * time.Hour).UTC().Truncate(time.Second) + err := p.SetContinuitySessionExpiry(ctx, expected.ID, newExpiry) + require.ErrorIs(t, err, sqlcon.ErrNoRows) + }) + }) + t.Run("case=cleanup", func(t *testing.T) { id := x.NewUUID() yesterday := time.Now().Add(-24 * time.Hour).UTC().Truncate(time.Second) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/persistence/sql/persister_continuity.go b/persistence/sql/persister_continuity.go index 73078784766c..ee7a4597e469 100644 --- a/persistence/sql/persister_continuity.go +++ b/persistence/sql/persister_continuity.go @@ -28,6 +28,23 @@ func (p *Persister) SaveContinuitySession(ctx context.Context, c *continuity.Con return sqlcon.HandleError(p.GetConnection(ctx).Create(c)) } +func (p *Persister) SetContinuitySessionExpiry(ctx context.Context, id uuid.UUID, expiresAt time.Time) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.SetContinuitySessionExpiry") + defer otelx.End(span, &err) + + if rows, err := p.GetConnection(ctx). + Where("id = ? AND nid = ?", id, p.NetworkID(ctx)). + UpdateQuery(&continuity.Container{ + ExpiresAt: expiresAt, + }, "expires_at"); err != nil { + return sqlcon.HandleError(err) + } else if rows == 0 { + return errors.WithStack(sqlcon.ErrNoRows) + } + + return nil +} + func (p *Persister) GetContinuitySession(ctx context.Context, id uuid.UUID) (_ *continuity.Container, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetContinuitySession") defer otelx.End(span, &err) diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 449bfece3878..6515d06367ee 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -14,6 +14,7 @@ import ( "net/url" "path/filepath" "strings" + "time" "golang.org/x/exp/maps" @@ -316,7 +317,10 @@ func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flo cntnr := AuthCodeContainer{} if f.GetType() == flow.TypeBrowser || !hasSessionTokenCode { - if _, err := s.d.ContinuityManager().Continue(r.Context(), w, r, sessionName, continuity.WithPayload(&cntnr)); err != nil { + if _, err := s.d.ContinuityManager().Continue(r.Context(), w, r, sessionName, + continuity.WithPayload(&cntnr), + continuity.WithExpireInsteadOfDelete(time.Minute), + ); err != nil { return nil, nil, err } if stateParam != cntnr.State { @@ -334,6 +338,7 @@ func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flo if errorParam != "" { return f, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider returned error "%s": %s`, r.URL.Query().Get("error"), r.URL.Query().Get("error_description"))) } + if codeParam == "" { return f, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the code query parameter.`)) } diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index b02e4fc45853..65c8f09b2e06 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -18,6 +18,9 @@ import ( "testing" "time" + "github.com/davecgh/go-spew/spew" + "github.com/samber/lo" + "github.com/ory/kratos/selfservice/hook/hooktest" "github.com/ory/x/sqlxx" @@ -495,6 +498,105 @@ func TestStrategy(t *testing.T) { postLoginWebhook.AssertTransientPayload(t, transientPayload) }) + + t.Run("case=should pass double submit", func(t *testing.T) { + // This test checks that the continuity manager uses a grace period to handle potential double-submit issues. + // + // It addresses issues where Facebook and Apple consent screens on mobile behave in a way that makes it + // easy for users to experience double-submit issues. + j, err := cookiejar.New(nil) + require.NoError(t, err) + + makeInitialRequest := func(t *testing.T, provider, action string, fv url.Values) (*http.Response, []byte, []string) { + fv.Set("provider", provider) + + var lastVia []*http.Request + hc := &http.Client{ + Jar: j, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + lastVia = via + return nil + }, + } + res, err := hc.PostForm(action, fv) + require.NoError(t, err, action) + + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + require.NotEmpty(t, lastVia) + + vias := make([]string, len(lastVia)) + for k, v := range lastVia { + vias[k] = v.URL.String() + } + + return res, body, vias + } + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + + // First login + res, body, via := makeInitialRequest(t, "valid", action, url.Values{}) + assertIdentity(t, res, body) + expectTokens(t, "valid", body) + assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + // We fetch the URL which includes the `?code` query parameter. + result := lo.Filter(via, func(s string, _ int) bool { + return strings.Contains(s, "code=") + }) + require.Len(t, result, 1) + + // And call that URL again. What's interesting here is that the whole requets passes because we are already authenticated. + // + // In this scenario, Ory Kratos correctly forwards the user to the return URL, which in our case returns the identity. + // + // We essentially run into this bit: + // + // if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { + // s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + // } else if authenticated { + // return <-- we end up here on the second call + // } + res, err = (&http.Client{Jar: j}).Get(result[0]) + require.NoError(t, err) + body, err = io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + assertIdentity(t, res, body) + expectTokens(t, "valid", body) + assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + // Trying this flow again without the Ory Session cookie will fail as we run into code reuse: + cookies := j.Cookies(urlx.ParseOrPanic(ts.URL)) + t.Logf("Cookies: %s", spew.Sdump(cookies)) + + secondJar, err := cookiejar.New(nil) + require.NoError(t, err) + + secondJar.SetCookies(urlx.ParseOrPanic(ts.URL), lo.Filter(cookies, func(item *http.Cookie, index int) bool { + return item.Name != "ory_kratos_session" + })) + + cookies = secondJar.Cookies(urlx.ParseOrPanic(ts.URL)) + t.Logf("Cookies after: %s", spew.Sdump(cookies)) + + // Doing the request but this time without the Ory Session Cookie. This may be the case in scenarios where we run into race conditions + // where the server sent a response but the client did not process it. + res, err = (&http.Client{Jar: secondJar}).Get(result[0]) + require.NoError(t, err) + body, err = io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + // The reason for `invalid_client` here is that the code was already used and the session was already authenticated. The invalid_client + // happens because of the way Golang's OAuth2 library is trying out different auth methods when a token request fails, which obfuscates + // the underlying error. + assert.Contains(t, string(body), "invalid_client", "%s", body) + }) }) t.Run("case=login without registered account", func(t *testing.T) { diff --git a/x/cookie.go b/x/cookie.go index 4172d2878cf5..897401183c0e 100644 --- a/x/cookie.go +++ b/x/cookie.go @@ -5,6 +5,7 @@ package x import ( "net/http" + "time" "github.com/gorilla/sessions" "github.com/pkg/errors" @@ -71,6 +72,17 @@ func SessionUnset(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, return errors.WithStack(cookie.Save(r, w)) } +func SessionSetExpiresIn(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, id string, expiresIn time.Duration) error { + cookie, err := s.Get(r, id) + if err == nil && cookie.IsNew { + // No cookie was sent in the request. We have nothing to do. + return nil + } + + cookie.Options.MaxAge = int(expiresIn.Seconds()) + return errors.WithStack(cookie.Save(r, w)) +} + func SessionUnsetKey(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, id, key string) error { cookie, err := s.Get(r, id) if err == nil && cookie.IsNew { From 5dcbb77ccac2143c9098d964cc438249ac8775ec Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 17 May 2024 12:37:51 +0000 Subject: [PATCH 101/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 83792ef814bf911405cdaf72b11c1dda2e472f80 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 21 May 2024 14:56:58 +0200 Subject: [PATCH 102/262] chore: allow smtp jim config (#3932) --- x/mailhog.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/x/mailhog.go b/x/mailhog.go index 8a54f5a35ed4..6f5537ba73d8 100644 --- a/x/mailhog.go +++ b/x/mailhog.go @@ -32,7 +32,7 @@ func CleanUpTestSMTP() { resources = nil } -func RunTestSMTP() (smtp, api string, err error) { +func RunTestSMTP(options ...string) (smtp, api string, err error) { if smtp, api := os.Getenv("TEST_MAILHOG_SMTP"), os.Getenv("TEST_MAILHOG_API"); smtp != "" && api != "" { return smtp, api, nil } else if len(smtp)+len(api) > 0 { @@ -53,20 +53,24 @@ func RunTestSMTP() (smtp, api string, err error) { } smtpPort, apiPort := ports[0], ports[1] + if len(options) == 0 { + options = []string{ + "-invite-jim", + "-jim-linkspeed-affect=0.05", + "-jim-reject-auth=0.05", + "-jim-reject-recipient=0.05", + "-jim-reject-sender=0.05", + "-jim-disconnect=0.05", + "-jim-linkspeed-min=1250", + "-jim-linkspeed-max=12500", + } + } + resource, err := pool. RunWithOptions(&dockertest.RunOptions{ Repository: "mailhog/mailhog", Tag: "v1.0.0", - Cmd: []string{ - "-invite-jim", - "-jim-linkspeed-affect=0.05", - "-jim-reject-auth=0.05", - "-jim-reject-recipient=0.05", - "-jim-reject-sender=0.05", - "-jim-disconnect=0.05", - "-jim-linkspeed-min=1250", - "-jim-linkspeed-max=12500", - }, + Cmd: options, PortBindings: map[docker.Port][]docker.PortBinding{ "8025/tcp": {{HostPort: fmt.Sprintf("%d/tcp", apiPort)}}, "1025/tcp": {{HostPort: fmt.Sprintf("%d/tcp", smtpPort)}}, From 9730e099a656d211389d8e993c64d8082784c929 Mon Sep 17 00:00:00 2001 From: Jack Williams <155615316+jacwil@users.noreply.github.com> Date: Tue, 21 May 2024 08:19:05 -0700 Subject: [PATCH 103/262] fix: change return urls in quickstarts (#3928) --- contrib/quickstart/kratos/email-password/kratos.yml | 4 ++-- contrib/quickstart/kratos/passkey/kratos.yml | 2 +- contrib/quickstart/kratos/phone-password/kratos.yml | 4 ++-- contrib/quickstart/kratos/webauthn/kratos.yml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/quickstart/kratos/email-password/kratos.yml b/contrib/quickstart/kratos/email-password/kratos.yml index d95e0ef68715..5597a1adcdb0 100644 --- a/contrib/quickstart/kratos/email-password/kratos.yml +++ b/contrib/quickstart/kratos/email-password/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome allowed_return_urls: - http://127.0.0.1:4455 - http://localhost:19006/Callback @@ -50,7 +50,7 @@ selfservice: ui_url: http://127.0.0.1:4455/verification use: code after: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome logout: after: diff --git a/contrib/quickstart/kratos/passkey/kratos.yml b/contrib/quickstart/kratos/passkey/kratos.yml index 6be298abd92e..776b17e3cab3 100644 --- a/contrib/quickstart/kratos/passkey/kratos.yml +++ b/contrib/quickstart/kratos/passkey/kratos.yml @@ -11,7 +11,7 @@ session: required_aal: aal1 selfservice: - default_browser_return_url: http://localhost:4455/ + default_browser_return_url: http://localhost:4455/welcome allowed_return_urls: - http://localhost:4455 - http://localhost:19006/Callback diff --git a/contrib/quickstart/kratos/phone-password/kratos.yml b/contrib/quickstart/kratos/phone-password/kratos.yml index 88ee01bc84e6..5826e4af6eb9 100644 --- a/contrib/quickstart/kratos/phone-password/kratos.yml +++ b/contrib/quickstart/kratos/phone-password/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome allowed_return_urls: - http://127.0.0.1:4455 - http://localhost:19006/Callback @@ -50,7 +50,7 @@ selfservice: ui_url: http://127.0.0.1:4455/verification use: code after: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome logout: after: diff --git a/contrib/quickstart/kratos/webauthn/kratos.yml b/contrib/quickstart/kratos/webauthn/kratos.yml index e3560a4dbc77..62168855cd7a 100644 --- a/contrib/quickstart/kratos/webauthn/kratos.yml +++ b/contrib/quickstart/kratos/webauthn/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://localhost:4455/ + default_browser_return_url: http://localhost:4455/welcome allowed_return_urls: - http://localhost:4455 @@ -58,7 +58,7 @@ selfservice: ui_url: http://localhost:4455/verification use: code after: - default_browser_return_url: http://localhost:4455/ + default_browser_return_url: http://localhost:4455/welcome logout: after: From de8e59c3091e85bf010dc942ede6a520e78d40b7 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Wed, 22 May 2024 07:31:39 +0000 Subject: [PATCH 104/262] chore: update repository templates to https://github.com/ory/meta/commit/e838bee8d0a29d022b61472be9efccf096099130 --- CONTRIBUTING.md | 14 +++++++++----- README.md | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c427d07090b..b061aced1b51 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,10 +144,12 @@ checklist to contribute an example: not get mixed up. 1. Add a descriptive prefix to commits. This ensures a uniform commit history and helps structure the changelog. Please refer to this - [list of prefixes for Kratos](https://github.com/ory/kratos/blob/master/.github/semantic.yml) - for an overview. + [Convential Commits configuration](https://github.com/ory/kratos/blob/master/.github/workflows/conventional_commits.yml) + for the list of accepted prefixes. You can read more about the Conventional + Commit specification + [at their site](https://www.conventionalcommits.org/en/v1.0.0/). 1. Create a `README.md` that explains how to use the example. (Use - [the README template](https://github.com/ory/examples/blob/master/_common/README)). + [the README template](https://github.com/ory/examples/blob/master/_common/README.md)). 1. Open a pull request and maintainers will review and merge your example. ## Contribute code @@ -172,8 +174,10 @@ request, go through this checklist: 1. Run `make format` 1. Add a descriptive prefix to commits. This ensures a uniform commit history and helps structure the changelog. Please refer to this - [list of prefixes for Kratos](https://github.com/ory/kratos/blob/master/.github/semantic.yml) - for an overview. + [Convential Commits configuration](https://github.com/ory/kratos/blob/master/.github/workflows/conventional_commits.yml) + for the list of accepted prefixes. You can read more about the Conventional + Commit specification + [at their site](https://www.conventionalcommits.org/en/v1.0.0/). If a pull request is not ready to be reviewed yet [it should be marked as a "Draft"](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request). diff --git a/README.md b/README.md index c74bb402836c..792ae8c5de79 100644 --- a/README.md +++ b/README.md @@ -554,7 +554,7 @@ that your company deserves a spot here, reach out to pinniped.dev - + Adopter * Pvotal From a14927dfa5f8d0fbda7e5a831f0a09a42369e06c Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Wed, 29 May 2024 12:21:58 +0200 Subject: [PATCH 105/262] test: resolve flaky e2e tests (#3935) * test: resolve flaky code registration tests * chore: don't fail logout if cookie is not found * chore: remove .only * chore: reduce wait * chore: u * chore: u * chore: u --- .github/workflows/ci.yaml | 12 +- .../profiles/code/login/success.spec.ts | 18 +++ .../code/registration/success.spec.ts | 134 ++++++++++-------- .../profiles/oidc/login/success.spec.ts | 2 +- .../two-steps/registration/code.spec.ts | 129 ++++++++--------- test/e2e/cypress/support/commands.ts | 7 +- test/e2e/profiles/code/.kratos.yml | 1 + test/e2e/run.sh | 16 ++- 8 files changed, 175 insertions(+), 144 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2d4e28d070dc..26df4ffc97a3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -150,12 +150,12 @@ jobs: name: Start CockroachDB - uses: browser-actions/setup-chrome@latest name: Install Chrome - - uses: browser-actions/setup-firefox@latest - name: Install Firefox - - uses: browser-actions/setup-geckodriver@latest - name: Install Geckodriver - with: - geckodriver-version: 0.32.0 + # - uses: browser-actions/setup-firefox@latest + # name: Install Firefox + # - uses: browser-actions/setup-geckodriver@latest + # name: Install Geckodriver + # with: + # geckodriver-version: 0.32.0 - uses: ory/ci/checkout@master with: fetch-depth: 2 diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts index 958f367612ab..94ce753f0322 100644 --- a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -138,6 +138,12 @@ context("Login success with code method", () => { cy.get(Selectors[app]["submit"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "mobile") { cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ @@ -206,6 +212,12 @@ context("Login success with code method", () => { cy.get(Selectors[app]["submit"]).click() + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "express") { cy.get('a[href*="sessions"').click() } @@ -274,6 +286,12 @@ context("Login success with code method", () => { cy.get(Selectors[app]["submit"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "mobile") { cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts index 53fd71ac0f66..c715d80cd86e 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -6,6 +6,53 @@ import { gen, MOBILE_URL } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" +const Selectors = { + mobile: { + identifier: "[data-testid='field/identifier']", + recoveryEmail: "[data-testid='field/email']", + email: "[data-testid='traits.email']", + email2: "[data-testid='traits.email2']", + tos: "[data-testid='traits.tos']", + username: "[data-testid='traits.username']", + code: "[data-testid='field/code'] input", + recoveryCode: "[data-testid='code']", + submitCode: "[data-testid='field/method/code']", + resendCode: "[data-testid='field/resend/code']", + submitRecovery: "[data-testid='field/method/code']", + codeHiddenMethod: "[data-testid='field/method/code']", + }, + express: { + identifier: "[data-testid='login-flow-code'] input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "[data-testid='registration-flow-code'] input[name='traits.email']", + email2: + "[data-testid='registration-flow-code'] input[name='traits.email2']", + tos: "[data-testid='registration-flow-code'] [name='traits.tos'] + label", + username: + "[data-testid='registration-flow-code'] input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + }, + react: { + identifier: "input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "input[name='traits.email']", + email2: "input[name='traits.email2']", + tos: "[name='traits.tos'] + label", + username: "input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + }, +} + context("Registration success with code method", () => { ;[ { @@ -31,57 +78,7 @@ context("Registration success with code method", () => { }, ].forEach(({ route, login, recovery, profile, app }) => { describe(`for app ${app}`, () => { - const Selectors = { - mobile: { - identifier: "[data-testid='field/identifier']", - recoveryEmail: "[data-testid='field/email']", - email: "[data-testid='traits.email']", - email2: "[data-testid='traits.email2']", - tos: "[data-testid='traits.tos']", - username: "[data-testid='traits.username']", - code: "[data-testid='field/code']", - recoveryCode: "[data-testid='code']", - submitCode: "[data-testid='field/method/code']", - resendCode: "[data-testid='field/method/resend']", - submitRecovery: "[data-testid='field/method/code']", - codeHiddenMethod: "[data-testid='field/method/code']", - }, - express: { - identifier: - "[data-testid='login-flow-code'] input[name='identifier']", - recoveryEmail: "input[name=email]", - email: - "[data-testid='registration-flow-code'] input[name='traits.email']", - email2: - "[data-testid='registration-flow-code'] input[name='traits.email2']", - tos: "[data-testid='registration-flow-code'] [name='traits.tos'] + label", - username: - "[data-testid='registration-flow-code'] input[name='traits.username']", - code: "input[name='code']", - recoveryCode: "input[name=code]", - submitRecovery: "button[name=method][value=code]", - submitCode: "button[name='method'][value='code']", - resendCode: "button[name='resend'][value='code']", - codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", - }, - react: { - identifier: "input[name='identifier']", - recoveryEmail: "input[name=email]", - email: "input[name='traits.email']", - email2: "input[name='traits.email2']", - tos: "[name='traits.tos'] + label", - username: "input[name='traits.username']", - code: "input[name='code']", - recoveryCode: "input[name=code]", - submitRecovery: "button[name=method][value=code]", - submitCode: "button[name='method'][value='code']", - resendCode: "button[name='resend'][value='code']", - codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", - }, - } - before(() => { - cy.deleteMail() cy.useConfigProfile(profile) if (app !== "mobile") { cy.proxy(app) @@ -94,11 +91,11 @@ context("Registration success with code method", () => { cy.visit(route) }) - it("should be able to resend the registration code", async () => { + it("should be able to resend the registration code", () => { const email = gen.email() cy.get(Selectors[app]["email"]).type(email) - cy.get(`${Selectors[app]["tos"]} + label`).click() + cy.get(Selectors[app]["tos"]).click() cy.submitCodeForm(app) cy.get('[data-testid="ui/message/1040005"]').should( @@ -110,7 +107,6 @@ context("Registration success with code method", () => { cy.wrap(code).as("code1"), ) - cy.get(Selectors[app]["email"]).should("have.value", email) cy.get(Selectors[app]["codeHiddenMethod"]).should("exist") cy.get(Selectors[app]["resendCode"]).click() @@ -180,7 +176,13 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["submitCode"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ expectAal: "aal1", @@ -190,9 +192,6 @@ context("Registration success with code method", () => { cy.wrap(session).as("session") }) }) - - cy.get('[data-testid="session-content"]').should("contain", email) - cy.get('[data-testid="session-token"]').should("not.be.empty") } else { cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( (session) => { @@ -236,7 +235,14 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["submitCode"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ expectAal: "aal1", @@ -246,9 +252,6 @@ context("Registration success with code method", () => { cy.wrap(session).as("session") }) }) - - cy.get('[data-testid="session-content"]').should("contain", email) - cy.get('[data-testid="session-token"]').should("not.be.empty") } else { cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( (session) => { @@ -305,6 +308,9 @@ context("Registration success with code method", () => { { hook: "session", }, + { + hook: "show_verification_ui", + }, ]) // Setup complex schema @@ -335,6 +341,7 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["submitCode"]).click() }, ) + cy.get('[data-testid="ui/message/1080003"]').should("be.visible") if (app === "mobile") { cy.visit(MOBILE_URL + "/Home") @@ -359,8 +366,14 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["code"]).type(code) cy.get(Selectors[app]["submitCode"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ expectAal: "aal1", @@ -370,9 +383,6 @@ context("Registration success with code method", () => { cy.wrap(session).as("session") }) }) - - cy.get('[data-testid="session-content"]').should("contain", email) - cy.get('[data-testid="session-token"]').should("not.be.empty") } else { cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( (session) => { diff --git a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts index 866f4344eda6..8e381e7acf5d 100644 --- a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts @@ -39,7 +39,7 @@ context("Social Sign In Successes", () => { cy.loginOidc({ app, url: login }) }) - it.only("should be able to sign up and link existing account", () => { + it("should be able to sign up and link existing account", () => { const email = gen.email() const password = gen.password() diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts index 2b09fa1e6745..bffafb36ee03 100644 --- a/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts @@ -6,6 +6,57 @@ import { gen, MOBILE_URL } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" +const Selectors = { + mobile: { + identifier: "[data-testid='field/identifier']", + recoveryEmail: "[data-testid='field/email']", + email: "[data-testid='traits.email']", + email2: "[data-testid='traits.email2']", + website: "[data-testid='traits.website']", + username: "[data-testid='traits.username']", + code: "[data-testid='field/code'] input", + recoveryCode: "[data-testid='code']", + submitCode: "[data-testid='field/method/code']", + resendCode: "[data-testid='field/resend/code']", + credentialSelection: "[data-testid='field/screen/credential-selection']", + submitRecovery: "[data-testid='field/method/code']", + codeHiddenMethod: "[data-testid='field/method/code']", + }, + express: { + identifier: "[data-testid='login-flow-code'] input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "[data-testid='node/input/traits.email'] input[name='traits.email']", + email2: + "[data-testid='node/input/traits.email2'] input[name='traits.email2']", + website: + "[data-testid='node/input/traits.website'] [name='traits.website']", + username: + "[data-testid='node/input/traits.username'] input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + credentialSelection: "[name='screen'][value='credential-selection']", + }, + react: { + identifier: "input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "input[name='traits.email']", + email2: "input[name='traits.email2']", + website: "[name='traits.website']", + username: "input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + credentialSelection: "[name='screen'][value='credential-selection']", + }, +} + context("Registration success with code method", () => { ;[ { @@ -31,62 +82,7 @@ context("Registration success with code method", () => { }, ].forEach(({ route, login, recovery, profile, app }) => { describe(`for app ${app}`, () => { - const Selectors = { - mobile: { - identifier: "[data-testid='field/identifier']", - recoveryEmail: "[data-testid='field/email']", - email: "[data-testid='traits.email']", - email2: "[data-testid='traits.email2']", - website: "[data-testid='traits.website']", - username: "[data-testid='traits.username']", - code: "[data-testid='field/code'] input", - recoveryCode: "[data-testid='code']", - submitCode: "[data-testid='field/method/code']", - resendCode: "[data-testid='field/resend/code']", - credentialSelection: - "[data-testid='field/screen/credential-selection']", - submitRecovery: "[data-testid='field/method/code']", - codeHiddenMethod: "[data-testid='field/method/code']", - }, - express: { - identifier: - "[data-testid='login-flow-code'] input[name='identifier']", - recoveryEmail: "input[name=email]", - email: - "[data-testid='node/input/traits.email'] input[name='traits.email']", - email2: - "[data-testid='node/input/traits.email2'] input[name='traits.email2']", - website: - "[data-testid='node/input/traits.website'] [name='traits.website']", - username: - "[data-testid='node/input/traits.username'] input[name='traits.username']", - code: "input[name='code']", - recoveryCode: "input[name=code]", - submitRecovery: "button[name=method][value=code]", - submitCode: "button[name='method'][value='code']", - resendCode: "button[name='resend'][value='code']", - codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", - credentialSelection: "[name='screen'][value='credential-selection']", - }, - react: { - identifier: "input[name='identifier']", - recoveryEmail: "input[name=email]", - email: "input[name='traits.email']", - email2: "input[name='traits.email2']", - website: "[name='traits.website']", - username: "input[name='traits.username']", - code: "input[name='code']", - recoveryCode: "input[name=code]", - submitRecovery: "button[name=method][value=code]", - submitCode: "button[name='method'][value='code']", - resendCode: "button[name='resend'][value='code']", - codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", - credentialSelection: "[name='screen'][value='credential-selection']", - }, - } - before(() => { - cy.deleteMail() cy.useConfigProfile(profile) if (app !== "mobile") { cy.proxy(app) @@ -94,12 +90,12 @@ context("Registration success with code method", () => { }) beforeEach(() => { - cy.deleteMail() + cy.deleteMail({ atLeast: 0 }) cy.clearAllCookies() cy.visit(route) }) - it("should be able to resend the registration code", async () => { + it("should be able to resend the registration code", () => { const email = gen.email() const website = "https://www.example.org/" @@ -141,12 +137,9 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["credentialSelection"]).click() cy.submitCodeForm(app) - // Mobile app sends another email when we go back and forth. - if (app === "mobile") { - cy.getRegistrationCodeFromEmail(email).then((code) => { - cy.wrap(code).as("code2") - }) - } + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.wrap(code).as("code2") + }) cy.get("@code2").then((code2) => { cy.get(Selectors[app]["code"]).clear() @@ -154,7 +147,14 @@ context("Registration success with code method", () => { cy.submitCodeForm(app) }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ expectAal: "aal1", @@ -164,9 +164,6 @@ context("Registration success with code method", () => { cy.wrap(session).as("session") }) }) - - cy.get('[data-testid="session-content"]').should("contain", email) - cy.get('[data-testid="session-token"]').should("not.be.empty") } else { cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( (session) => { diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index cfaef5ac9185..0b4584646abc 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -845,7 +845,7 @@ Cypress.Commands.add( if (expectSession) { // for some reason react flakes here although the login succeeded and there should be a session it fails if (app === "react") { - cy.wait(2000) // adding arbitrary wait here. not sure if there is a better way in this case + cy.wait(500) // adding arbitrary wait here. not sure if there is a better way in this case } cy.getSession() } else { @@ -922,8 +922,9 @@ Cypress.Commands.add("logout", () => { const c = cookies.find( ({ name }) => name.indexOf("ory_kratos_session") > -1, ) - expect(c).to.not.be.undefined - cy.clearCookie(c.name) + if (c) { + cy.clearCookie(c.name) + } }) cy.noSession() }) diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 0db7cd92b1ca..3e98857e1628 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -14,6 +14,7 @@ selfservice: after: code: hooks: + - hook: show_verification_ui - hook: session login: diff --git a/test/e2e/run.sh b/test/e2e/run.sh index c0af9664faa2..62b5330adafc 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -279,7 +279,7 @@ run() { if [ -z ${CYPRESS_RECORD_KEY+x} ]; then (cd test/e2e; npm run test --) else - (cd test/e2e; npm run test -- --record) + (cd test/e2e; npm run test -- --record --tag "${2}" ) fi fi } @@ -350,19 +350,23 @@ export TEST_DATABASE_MEMORY="memory" case "${1:-default}" in sqlite) echo "Database set up at: $TEST_DATABASE_SQLITE" - db="${TEST_DATABASE_SQLITE}" + dsn="${TEST_DATABASE_SQLITE}" + db="sqlite" ;; mysql) - db="${TEST_DATABASE_MYSQL}" + dsn="${TEST_DATABASE_MYSQL}" + db="mysql" ;; postgres) - db="${TEST_DATABASE_POSTGRESQL}" + dsn="${TEST_DATABASE_POSTGRESQL}" + db="postgres" ;; cockroach) - db="${TEST_DATABASE_COCKROACHDB}" + dsn="${TEST_DATABASE_COCKROACHDB}" + db="cockroach" ;; *) @@ -380,4 +384,4 @@ if [[ "${setup}" == "yes" ]]; then prepare fi -run "${db}" +run "${dsn}" "${db}" From 050a4dc3797261a10fa83228c482dd3b459779be Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 3 Jun 2024 12:00:29 +0200 Subject: [PATCH 106/262] chore: upgrade nyaruka/phonenumbers to v1.3.6 (#3940) --- go.mod | 6 +++--- go.sum | 12 ++++++------ identity/extension_verification_test.go | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 8f8d3c1e60ec..b15b91816bf4 100644 --- a/go.mod +++ b/go.mod @@ -100,7 +100,7 @@ require ( go.opentelemetry.io/otel/sdk v1.21.0 go.opentelemetry.io/otel/trace v1.22.0 golang.org/x/crypto v0.22.0 - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 golang.org/x/net v0.24.0 golang.org/x/oauth2 v0.16.0 golang.org/x/sync v0.5.0 @@ -253,7 +253,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect - github.com/nyaruka/phonenumbers v1.1.6 // indirect + github.com/nyaruka/phonenumbers v1.3.6 // indirect github.com/ogier/pflag v0.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -310,7 +310,7 @@ require ( golang.org/x/mod v0.14.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect - golang.org/x/tools v0.15.0 // indirect + golang.org/x/tools v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect diff --git a/go.sum b/go.sum index 964e696c2c4b..864128132731 100644 --- a/go.sum +++ b/go.sum @@ -785,8 +785,8 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= -github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= +github.com/nyaruka/phonenumbers v1.3.6 h1:33owXWp4d1U+Tyaj9fpci6PbvaQZcXBUO2FybeKeLwQ= +github.com/nyaruka/phonenumbers v1.3.6/go.mod h1:Ut+eFwikULbmCenH6InMKL9csUNLyxHuBLyfkpum11s= github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -1129,8 +1129,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1435,8 +1435,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools/cmd/cover v0.1.0-deprecated h1:Rwy+mWYz6loAF+LnG1jHG/JWMHRMMC2/1XX3Ejkx9lA= golang.org/x/tools/cmd/cover v0.1.0-deprecated/go.mod h1:hMDiIvlpN1NoVgmjLjUJE9tMHyxHjFX7RuQ+rW12mSA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/identity/extension_verification_test.go b/identity/extension_verification_test.go index ebf2c09f207e..cc7f47b07091 100644 --- a/identity/extension_verification_test.go +++ b/identity/extension_verification_test.go @@ -369,6 +369,21 @@ func TestSchemaExtensionVerification(t *testing.T) { }, }, }, + { + // see https://github.com/ory/kratos/issues/3933 + name: "phone:should parse +16453331111", + schema: phoneSchemaPath, + doc: `{"phones":["+16453331111"]}`, + expect: []VerifiableAddress{ + { + Value: "+16453331111", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, } { t.Run(fmt.Sprintf("case=%v", tc.name), func(t *testing.T) { id := &Identity{ID: iid, VerifiableAddresses: tc.existing} @@ -385,6 +400,8 @@ func TestSchemaExtensionVerification(t *testing.T) { if tc.expectErr != nil { require.EqualError(t, err, tc.expectErr.Error()) return + } else { + require.NoError(t, err) } require.NoError(t, e.Finish()) From fbbac77e208fd08a3d293a9a781bf7a9f3314446 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:08:12 +0200 Subject: [PATCH 107/262] chore: improve courier logging (#3943) --- courier/courier_dispatcher.go | 55 ++++++++++++++++------------------- courier/http_channel.go | 23 ++++++++------- courier/smtp_channel.go | 45 +++++++++++++--------------- 3 files changed, 57 insertions(+), 66 deletions(-) diff --git a/courier/courier_dispatcher.go b/courier/courier_dispatcher.go index 3d4835636206..47399369c3c3 100644 --- a/courier/courier_dispatcher.go +++ b/courier/courier_dispatcher.go @@ -10,11 +10,16 @@ import ( ) func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { + logger := c.deps.Logger(). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + if err := c.deps.CourierPersister().IncrementMessageSendCount(ctx, msg.ID); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to increment the message's "send_count" field`) return err } @@ -24,28 +29,21 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { return errors.Errorf("message %s has unknown channel %q", msg.ID.String(), msg.Channel) } + logger = logger. + WithField("channel", channel.ID()) + if err := channel.Dispatch(ctx, msg); err != nil { return err } if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusSent); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - WithField("channel", channel.ID()). Error(`Unable to set the message status to "sent".`) return err } - c.deps.Logger(). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - WithField("channel", channel.ID()). - Debug("Courier sent out message.") + logger.Debug("Courier sent out message.") return nil } @@ -63,27 +61,28 @@ func (c *courier) DispatchQueue(ctx context.Context) error { } for k, msg := range messages { + logger := c.deps.Logger(). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + if msg.SendCount > maxRetries { if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to set the retried message's status to "abandoned".`) return err } // Skip the message - c.deps.Logger(). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). + logger. Warnf(`Message was abandoned because it did not deliver after %d attempts`, msg.SendCount) } else if err := c.DispatchMessage(ctx, msg); err != nil { if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusFailed, err); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to record failure log entry.`) if c.failOnDispatchError { return err @@ -92,10 +91,8 @@ func (c *courier) DispatchQueue(ctx context.Context) error { for _, replace := range messages[k:] { if err := c.deps.CourierPersister().SetMessageStatus(ctx, replace.ID, MessageStatusQueued); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", replace.ID). - WithField("message_nid", replace.NID). Error(`Unable to reset the failed message's status to "queued".`) if c.failOnDispatchError { return err @@ -107,10 +104,8 @@ func (c *courier) DispatchQueue(ctx context.Context) error { return err } } else if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusSuccess, nil); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to record success log entry.`) // continue with execution, as the message was successfully dispatched } diff --git a/courier/http_channel.go b/courier/http_channel.go index 13e0a5792623..2e405fb22abe 100644 --- a/courier/http_channel.go +++ b/courier/http_channel.go @@ -8,6 +8,8 @@ import ( "encoding/json" "fmt" + "github.com/tidwall/gjson" + "github.com/pkg/errors" "github.com/ory/kratos/courier/template" @@ -89,13 +91,16 @@ func (c *httpChannel) Dispatch(ctx context.Context, msg Message) (err error) { return errors.WithStack(err) } + logger := c.d.Logger(). + WithField("http_server", gjson.GetBytes(c.requestConfig, "url").String()). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + if res.StatusCode >= 200 && res.StatusCode < 300 { - c.d.Logger(). - WithField("message_id", msg.ID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - Debug("Courier sent out mailer.") + logger.Debug("Courier sent out mailer.") return nil } @@ -103,11 +108,7 @@ func (c *httpChannel) Dispatch(ctx context.Context, msg Message) (err error) { "unable to dispatch mail delivery because upstream server replied with status code %d", res.StatusCode, ) - c.d.Logger(). - WithField("message_id", msg.ID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). + logger. WithError(err). Error("sending mail via HTTP failed.") return errors.WithStack(err) diff --git a/courier/smtp_channel.go b/courier/smtp_channel.go index a44719a351d6..9ed9335f8e7f 100644 --- a/courier/smtp_channel.go +++ b/courier/smtp_channel.go @@ -65,6 +65,10 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { } } + if cfg == nil { + return errors.WithStack(herodot.ErrInternalServerError.WithErrorf("Courier tried to deliver an email but SMTP channel is misconfigured.")) + } + gm := mail.NewMessage() if cfg.FromName == "" { gm.SetHeader("From", cfg.FromAddress) @@ -82,19 +86,23 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { gm.SetBody("text/plain", msg.Body) + logger := c.d.Logger(). + WithField("smtp_server", fmt.Sprintf("%s:%d", c.smtpClient.Host, c.smtpClient.Port)). + WithField("smtp_ssl_enabled", c.smtpClient.SSL). + WithField("message_from", cfg.FromAddress). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + tmpl, err := c.newEmailTemplateFromMessage(c.d, msg) if err != nil { - c.d.Logger(). - WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error(`Unable to get email template from message.`) + logger. + WithError(err).Error(`Unable to get email template from message.`) } else if htmlBody, err := tmpl.EmailBody(ctx); err != nil { - c.d.Logger(). - WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error(`Unable to get email body from template.`) + logger. + WithError(err).Error(`Unable to get email body from template.`) } else { gm.AddAlternative("text/html", htmlBody) } @@ -102,11 +110,6 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { if err := c.smtpClient.DialAndSend(ctx, gm); err != nil { c.d.Logger(). WithError(err). - WithField("smtp_server", fmt.Sprintf("%s:%d", c.smtpClient.Host, c.smtpClient.Port)). - WithField("smtp_ssl_enabled", c.smtpClient.SSL). - WithField("message_from", cfg.FromAddress). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error("Unable to send email using SMTP connection.") var protoErr *textproto.Error @@ -119,10 +122,8 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { // See https://en.wikipedia.org/wiki/List_of_SMTP_server_return_codes // If the SMTP server responds with 5xx, sending the message should not be retried (without changing something about the request) if err := c.d.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { - c.d.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to reset the retried message's status to "abandoned".`) return err } @@ -132,13 +133,7 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { WithError(err.Error()).WithReason("failed to send email via smtp")) } - c.d.Logger(). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - Debug("Courier sent out message.") + logger.Debug("Courier sent out message.") return nil } From 25d1ecd90317193095e01b97ff21d92920035b02 Mon Sep 17 00:00:00 2001 From: Patrik Date: Tue, 4 Jun 2024 12:26:27 +0200 Subject: [PATCH 108/262] feat: allow admin to create API code recovery flows (#3939) --- internal/client-go/go.sum | 1 + ..._create_recovery_code_for_identity_body.go | 37 +++++++ ..._create_recovery_code_for_identity_body.go | 37 +++++++ selfservice/flow/type.go | 8 ++ ...n=should_fail_on_negative_expiry_time.json | 2 +- .../strategy/code/strategy_recovery.go | 9 +- .../strategy/code/strategy_recovery_admin.go | 22 +++- .../code/strategy_recovery_admin_test.go | 102 ++++++++++++------ .../strategy/link/strategy_recovery.go | 5 +- .../strategy/link/strategy_recovery_test.go | 42 +++----- spec/api.json | 3 + spec/swagger.json | 3 + 12 files changed, 196 insertions(+), 75 deletions(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_create_recovery_code_for_identity_body.go b/internal/client-go/model_create_recovery_code_for_identity_body.go index 850c7e086206..2947fad34e51 100644 --- a/internal/client-go/model_create_recovery_code_for_identity_body.go +++ b/internal/client-go/model_create_recovery_code_for_identity_body.go @@ -19,6 +19,8 @@ import ( type CreateRecoveryCodeForIdentityBody struct { // Code Expires In The recovery code will expire after that amount of time has passed. Defaults to the configuration value of `selfservice.methods.code.config.lifespan`. ExpiresIn *string `json:"expires_in,omitempty"` + // The flow type can either be `api` or `browser`. + FlowType *string `json:"flow_type,omitempty"` // Identity to Recover The identity's ID you wish to recover. IdentityId string `json:"identity_id"` } @@ -73,6 +75,38 @@ func (o *CreateRecoveryCodeForIdentityBody) SetExpiresIn(v string) { o.ExpiresIn = &v } +// GetFlowType returns the FlowType field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowType() string { + if o == nil || o.FlowType == nil { + var ret string + return ret + } + return *o.FlowType +} + +// GetFlowTypeOk returns a tuple with the FlowType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowTypeOk() (*string, bool) { + if o == nil || o.FlowType == nil { + return nil, false + } + return o.FlowType, true +} + +// HasFlowType returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasFlowType() bool { + if o != nil && o.FlowType != nil { + return true + } + + return false +} + +// SetFlowType gets a reference to the given string and assigns it to the FlowType field. +func (o *CreateRecoveryCodeForIdentityBody) SetFlowType(v string) { + o.FlowType = &v +} + // GetIdentityId returns the IdentityId field value func (o *CreateRecoveryCodeForIdentityBody) GetIdentityId() string { if o == nil { @@ -102,6 +136,9 @@ func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { if o.ExpiresIn != nil { toSerialize["expires_in"] = o.ExpiresIn } + if o.FlowType != nil { + toSerialize["flow_type"] = o.FlowType + } if true { toSerialize["identity_id"] = o.IdentityId } diff --git a/internal/httpclient/model_create_recovery_code_for_identity_body.go b/internal/httpclient/model_create_recovery_code_for_identity_body.go index 850c7e086206..2947fad34e51 100644 --- a/internal/httpclient/model_create_recovery_code_for_identity_body.go +++ b/internal/httpclient/model_create_recovery_code_for_identity_body.go @@ -19,6 +19,8 @@ import ( type CreateRecoveryCodeForIdentityBody struct { // Code Expires In The recovery code will expire after that amount of time has passed. Defaults to the configuration value of `selfservice.methods.code.config.lifespan`. ExpiresIn *string `json:"expires_in,omitempty"` + // The flow type can either be `api` or `browser`. + FlowType *string `json:"flow_type,omitempty"` // Identity to Recover The identity's ID you wish to recover. IdentityId string `json:"identity_id"` } @@ -73,6 +75,38 @@ func (o *CreateRecoveryCodeForIdentityBody) SetExpiresIn(v string) { o.ExpiresIn = &v } +// GetFlowType returns the FlowType field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowType() string { + if o == nil || o.FlowType == nil { + var ret string + return ret + } + return *o.FlowType +} + +// GetFlowTypeOk returns a tuple with the FlowType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowTypeOk() (*string, bool) { + if o == nil || o.FlowType == nil { + return nil, false + } + return o.FlowType, true +} + +// HasFlowType returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasFlowType() bool { + if o != nil && o.FlowType != nil { + return true + } + + return false +} + +// SetFlowType gets a reference to the given string and assigns it to the FlowType field. +func (o *CreateRecoveryCodeForIdentityBody) SetFlowType(v string) { + o.FlowType = &v +} + // GetIdentityId returns the IdentityId field value func (o *CreateRecoveryCodeForIdentityBody) GetIdentityId() string { if o == nil { @@ -102,6 +136,9 @@ func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { if o.ExpiresIn != nil { toSerialize["expires_in"] = o.ExpiresIn } + if o.FlowType != nil { + toSerialize["flow_type"] = o.FlowType + } if true { toSerialize["identity_id"] = o.IdentityId } diff --git a/selfservice/flow/type.go b/selfservice/flow/type.go index 2f0726b0352e..4206344ed7a9 100644 --- a/selfservice/flow/type.go +++ b/selfservice/flow/type.go @@ -22,3 +22,11 @@ func (t Type) IsBrowser() bool { func (t Type) IsAPI() bool { return t == TypeAPI } + +func (t Type) Valid() bool { + switch t { + case TypeAPI, TypeBrowser: + return true + } + return false +} diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json index 0ebac1807073..e9751eea8e8f 100644 --- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json @@ -2,7 +2,7 @@ "error": { "code": 400, "message": "The request was malformed or contained invalid parameters", - "reason": "Value from \"expires_in\" must result to a future time: -1h", + "reason": "Value from \"expires_in\" must result to a future time: -1h0m0s", "status": "Bad Request" } } diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 0cbddf393325..758e81d04fd9 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -161,9 +161,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch recoveryFlow.State { - case flow.StateChooseMethod: - fallthrough - case flow.StateEmailSent: + case flow.StateChooseMethod, + flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, recoveryFlow, body) case flow.StatePassedChallenge: // was already handled, do not allow retry @@ -237,9 +236,7 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, if s.deps.Config().UseContinueWithTransitions(ctx) { switch { - case f.Type.IsAPI(): - fallthrough - case x.IsJSONRequest(r): + case f.Type.IsAPI(), x.IsJSONRequest(r): f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf)) s.deps.Writer().Write(w, r, f) default: diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go index 8964682afe30..028bb811bcaa 100644 --- a/selfservice/strategy/code/strategy_recovery_admin.go +++ b/selfservice/strategy/code/strategy_recovery_admin.go @@ -73,6 +73,13 @@ type createRecoveryCodeForIdentityBody struct { // - 1m // - 1s ExpiresIn string `json:"expires_in"` + + // Flow Type + // + // The flow type for the recovery flow. Defaults to browser. + // + // required: false + FlowType *flow.Type `json:"flow_type"` } // Recovery Code for Identity @@ -149,12 +156,21 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. } } - if time.Now().Add(expiresIn).Before(time.Now()) { - s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "expires_in" must result to a future time: %s`, p.ExpiresIn))) + if expiresIn <= 0 { + s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "expires_in" must result to a future time: %s`, expiresIn))) + return + } + + flowType := flow.TypeBrowser + if p.FlowType != nil { + flowType = *p.FlowType + } + if !flowType.Valid() { + s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "flow_type" is not valid: %q`, flowType))) return } - recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) + recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flowType) if err != nil { s.deps.Writer().WriteError(w, r, err) return diff --git a/selfservice/strategy/code/strategy_recovery_admin_test.go b/selfservice/strategy/code/strategy_recovery_admin_test.go index aed7bbcbaf43..882831b06b14 100644 --- a/selfservice/strategy/code/strategy_recovery_admin_test.go +++ b/selfservice/strategy/code/strategy_recovery_admin_test.go @@ -8,8 +8,10 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/cookiejar" "net/http/httptest" "net/url" + "strings" "testing" "time" @@ -18,13 +20,14 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" - "github.com/ory/kratos/selfservice/strategy/code" + . "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/x" "github.com/ory/x/ioutilx" "github.com/ory/x/pointerx" @@ -35,6 +38,7 @@ func TestAdminStrategy(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) initViper(t, ctx, conf) + conf.MustSet(ctx, config.ViperKeyUseContinueWithTransitions, true) _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) @@ -44,14 +48,11 @@ func TestAdminStrategy(t *testing.T) { publicTS, adminTS := testhelpers.NewKratosServer(t, reg) adminSDK := testhelpers.NewSDKClient(adminTS) - createCode := func(id string, expiresIn *string) (*kratos.RecoveryCodeForIdentity, *http.Response, error) { + type createCodeParams = kratos.CreateRecoveryCodeForIdentityBody + createCode := func(params createCodeParams) (*kratos.RecoveryCodeForIdentity, *http.Response, error) { return adminSDK.IdentityApi. CreateRecoveryCodeForIdentity(context.Background()). - CreateRecoveryCodeForIdentityBody( - kratos.CreateRecoveryCodeForIdentityBody{ - IdentityId: id, - ExpiresIn: expiresIn, - }).Execute() + CreateRecoveryCodeForIdentityBody(params).Execute() } t.Run("no panic on empty body #1384", func(t *testing.T) { @@ -63,39 +64,42 @@ func TestAdminStrategy(t *testing.T) { f, err := recovery.NewFlow(reg.Config(), time.Minute, "", r, s, flow.TypeBrowser) require.NoError(t, err) require.NotPanics(t, func() { - require.Error(t, s.(*code.Strategy).HandleRecoveryError(w, r, f, nil, errors.New("test"))) + require.Error(t, s.(*Strategy).HandleRecoveryError(w, r, f, nil, errors.New("test"))) }) }) t.Run("description=should not be able to recover an account that does not exist", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), nil) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String()}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) t.Run("description=should fail on malformed expiry time", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), pointerx.String("not-a-valid-value")) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: pointerx.Ptr("not-a-valid-value")}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) t.Run("description=should fail on negative expiry time", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), pointerx.String("-1h")) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: pointerx.Ptr("-1h")}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) - submitRecoveryLink := func(t *testing.T, link string, code string) []byte { + submitRecoveryCode := func(t *testing.T, client *http.Client, link string, code string) []byte { t.Helper() - res, err := publicTS.Client().Get(link) + if client == nil { + client = publicTS.Client() + } + res, err := client.Get(link) require.NoError(t, err) body := ioutilx.MustReadAll(res.Body) action := gjson.GetBytes(body, "ui.action").String() require.NotEmpty(t, action) - res, err = publicTS.Client().PostForm(action, url.Values{ + res, err = client.PostForm(action, url.Values{ "code": {code}, }) require.NoError(t, err) @@ -104,13 +108,21 @@ func TestAdminStrategy(t *testing.T) { return ioutilx.MustReadAll(res.Body) } + assertEmailNotVerified := func(t *testing.T, email string) { + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, email) + assert.NoError(t, err) + assert.False(t, addr.Verified) + assert.Nil(t, addr.VerifiedAt) + assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + } + t.Run("description=should create code without email", func(t *testing.T) { id := identity.Identity{Traits: identity.Traits(`{}`)} require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), nil) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String()}) require.NoError(t, err) require.NotEmpty(t, code.RecoveryLink) @@ -119,8 +131,14 @@ func TestAdminStrategy(t *testing.T) { require.NotEmpty(t, code.RecoveryCode) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + client := pointerx.Ptr(*publicTS.Client()) + client.Jar, _ = cookiejar.New(nil) + body := submitRecoveryCode(t, client, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next 60.00 minutes.") + u, err := url.Parse(publicTS.URL) + cs := client.Jar.Cookies(u) + require.Len(t, cs, 1, "%s", body) + assert.Equal(t, "ory_kratos_session", cs[0].Name, "%s", body) }) t.Run("description=should not be able to recover with expired code", func(t *testing.T) { @@ -130,22 +148,18 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), pointerx.String("100ms")) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String(), ExpiresIn: pointerx.Ptr("100ms")}) require.NoError(t, err) time.Sleep(time.Millisecond * 100) require.NotEmpty(t, code.RecoveryLink) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + body := submitRecoveryCode(t, nil, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "The recovery flow expired 0.00 minutes ago, please try again.") // The recovery address should not be verified if the flow was initiated by the admins - addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) - assert.NoError(t, err) - assert.False(t, addr.Verified) - assert.Nil(t, addr.VerifiedAt) - assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + assertEmailNotVerified(t, recoveryEmail) }) t.Run("description=should create a valid recovery link and set the expiry time as well and recover the account", func(t *testing.T) { @@ -155,35 +169,32 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), nil) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String()}) require.NoError(t, err) require.NotEmpty(t, code.RecoveryLink) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)+time.Second))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + body := submitRecoveryCode(t, nil, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next 60.00 minutes.") - addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) - assert.NoError(t, err) - assert.False(t, addr.Verified) - assert.Nil(t, addr.VerifiedAt) - assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + // The recovery address should be verified if the flow was initiated by the admins + assertEmailNotVerified(t, recoveryEmail) }) t.Run("case=should not be able to use code from different flow", func(t *testing.T) { email := testhelpers.RandomEmail() i := createIdentityToRecover(t, reg, email) - c1, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c1, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) - c2, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c2, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) code2 := c2.RecoveryCode require.NotEmpty(t, code2) - body := submitRecoveryLink(t, c1.RecoveryLink, c2.RecoveryCode) + body := submitRecoveryCode(t, nil, c1.RecoveryLink, c2.RecoveryCode) testhelpers.AssertMessage(t, body, "The recovery code is invalid or has already been used. Please try again.") }) @@ -192,7 +203,7 @@ func TestAdminStrategy(t *testing.T) { email := testhelpers.RandomEmail() i := createIdentityToRecover(t, reg, email) - c1, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c1, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) res, err := http.Get(c1.RecoveryLink) @@ -201,4 +212,27 @@ func TestAdminStrategy(t *testing.T) { snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").String())) }) + + t.Run("case=should be able to create and complete an API flow", func(t *testing.T) { + email := testhelpers.RandomEmail() + i := createIdentityToRecover(t, reg, email) + + code, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), FlowType: pointerx.Ptr(string(flow.TypeAPI))}) + require.NoError(t, err) + + res, err := publicTS.Client().Get(code.RecoveryLink) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + + action := gjson.GetBytes(body, "ui.action").String() + require.NotEmpty(t, action) + + res, err = publicTS.Client().Post(action, "application/json", strings.NewReader(fmt.Sprintf(`{"code":"%s"}`, code.RecoveryCode))) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + continueWith := gjson.GetBytes(ioutilx.MustReadAll(res.Body), "continue_with").Array() + require.Len(t, continueWith, 2) + assert.EqualValues(t, flow.ContinueWithActionSetOrySessionTokenString, continueWith[0].Get("action").String()) + }) } diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index c8be6025f840..184399ca1002 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -283,9 +283,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch req.State { - case flow.StateChooseMethod: - fallthrough - case flow.StateEmailSent: + case flow.StateChooseMethod, + flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, req) case flow.StatePassedChallenge: // was already handled, do not allow retry diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 67e1cd388671..7b56ca5f1728 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -5,7 +5,6 @@ package link_test import ( "context" - _ "embed" "encoding/json" "fmt" "net/http" @@ -15,45 +14,32 @@ import ( "testing" "time" - "github.com/ory/kratos/driver" - "github.com/ory/kratos/session" - "github.com/davecgh/go-spew/spew" - "github.com/gofrs/uuid" - "github.com/pkg/errors" - - "github.com/ory/kratos/selfservice/flow" - "github.com/ory/kratos/selfservice/strategy/link" - - "github.com/ory/kratos/ui/node" - - kratos "github.com/ory/kratos/internal/httpclient" - - "github.com/ory/kratos/corpx" - - "github.com/ory/x/ioutilx" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/ory/x/urlx" - - "github.com/ory/x/sqlxx" - - "github.com/ory/x/assertx" - - "github.com/ory/x/pointerx" - + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/session" "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/pointerx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) func init() { @@ -128,7 +114,7 @@ func TestAdminStrategy(t *testing.T) { rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), - ExpiresIn: pointerx.String("100ms"), + ExpiresIn: pointerx.Ptr("100ms"), }).Execute() require.NoError(t, err) @@ -152,7 +138,7 @@ func TestAdminStrategy(t *testing.T) { rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), - ExpiresIn: pointerx.String("100ms"), + ExpiresIn: pointerx.Ptr("100ms"), }).Execute() require.NoError(t, err) diff --git a/spec/api.json b/spec/api.json index 48b0d934d382..365cc3fc5f7f 100644 --- a/spec/api.json +++ b/spec/api.json @@ -701,6 +701,9 @@ "pattern": "^([0-9]+(ns|us|ms|s|m|h))*$", "type": "string" }, + "flow_type": { + "$ref": "#/components/schemas/selfServiceFlowType" + }, "identity_id": { "description": "Identity to Recover\n\nThe identity's ID you wish to recover.", "format": "uuid", diff --git a/spec/swagger.json b/spec/swagger.json index 1d548df9a00f..9753c89c8991 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3824,6 +3824,9 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))*$" }, + "flow_type": { + "$ref": "#/definitions/selfServiceFlowType" + }, "identity_id": { "description": "Identity to Recover\n\nThe identity's ID you wish to recover.", "type": "string", From 3c0668989db55e4e31096486c146739725693d0a Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:28:01 +0000 Subject: [PATCH 109/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 2baecaeee334efed728b4f4c9a9a70e4695b0772 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:52:52 +0200 Subject: [PATCH 110/262] autogen: pin v1.2.0-pre.0 release commit --- .schemastore/config.schema.json | 127 +++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/.schemastore/config.schema.json b/.schemastore/config.schema.json index 829b71bae3fb..d7e725732f7d 100644 --- a/.schemastore/config.schema.json +++ b/.schemastore/config.schema.json @@ -75,6 +75,16 @@ "additionalProperties": false, "required": ["hook"] }, + "selfServiceVerificationHook": { + "type": "object", + "properties": { + "hook": { + "const": "verification" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, "selfServiceShowVerificationUIHook": { "type": "object", "properties": { @@ -253,6 +263,13 @@ "type": "string", "description": "The HTTP method to use (GET, POST, etc)." }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the Web-Hook", + "additionalProperties": { + "type": "string" + } + }, "body": { "type": "string", "oneOf": [ @@ -436,7 +453,9 @@ "dingtalk", "patreon", "linkedin", - "lark" + "linkedin_v2", + "lark", + "x" ], "examples": ["google"] }, @@ -733,6 +752,12 @@ }, { "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" } ] }, @@ -827,6 +852,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, "lookup_secret": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, @@ -860,6 +888,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterOIDCLoginMethod" }, @@ -885,6 +916,12 @@ { "$ref": "#/definitions/selfServiceRequireVerifiedAddressHook" }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" + }, { "$ref": "#/definitions/b2bSSOHook" } @@ -944,6 +981,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, @@ -1229,6 +1269,12 @@ }, "after": { "$ref": "#/definitions/selfServiceAfterRegistration" + }, + "enable_legacy_one_step": { + "type": "boolean", + "title": "Disable two-step registration", + "description": "Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To revert to one-step registration, set this to `true`.", + "default": false } } }, @@ -1688,6 +1734,67 @@ "required": ["config"] } }, + "passkey": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables the Passkey method", + "default": false + }, + "config": { + "type": "object", + "title": "Passkey Configuration", + "properties": { + "rp": { + "title": "Relying Party (RP) Config", + "properties": { + "display_name": { + "type": "string", + "title": "Relying Party Display Name", + "description": "A name to help the user identify this RP.", + "examples": ["Ory Foundation"] + }, + "id": { + "type": "string", + "title": "Relying Party Identifier", + "description": "The id must be a subset of the domain currently in the browser.", + "examples": ["ory.sh"] + }, + "origins": { + "type": "array", + "title": "Relying Party Origins", + "description": "A list of explicit RP origins. If left empty, this defaults to either `origin` or `id`, prepended with the current protocol schema (HTTP or HTTPS).", + "items": { + "type": "string", + "format": "uri", + "examples": [ + "https://www.ory.sh", + "https://auth.ory.sh" + ] + } + } + }, + "type": "object", + "required": ["display_name", "id"] + } + }, + "additionalProperties": false + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + }, + "then": { + "required": ["config"] + } + }, "oidc": { "type": "object", "title": "Specify OpenID Connect and OAuth2 Configuration", @@ -1952,7 +2059,6 @@ "default": "localhost" } }, - "required": ["connection_uri"], "additionalProperties": false }, "sms": { @@ -2305,7 +2411,7 @@ "additionalProperties": false }, "tracing": { - "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.614/otelx/config.schema.json" + "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.623/otelx/config.schema.json" }, "log": { "title": "Log", @@ -2731,7 +2837,7 @@ "properties": { "cacheable_sessions": { "type": "boolean", - "title": "Enable Ory Session Edge Caching", + "title": "Enable Ory Sessions caching", "description": "If enabled allows Ory Sessions to be cached. Only effective in the Ory Network.", "default": false }, @@ -2755,6 +2861,19 @@ "description": "Secifies which organizations are available. Only effective in the Ory Network.", "type": "array", "default": [] + }, + "enterprise": { + "title": "Enterprise features", + "description": "Specifies enterprise features. Only effective in the Ory Network or with a valid license.", + "type": "object", + "properties": { + "identity_schema_fallback_url_template": { + "type": "string", + "title": "Fallback URL template for identity schemas", + "description": "A fallback URL template used when looking up identity schemas." + } + }, + "additionalProperties": false } }, "allOf": [ From 1a70648c4d5b9b8d135dd7bea3842057e67b574e Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:50:31 +0200 Subject: [PATCH 111/262] autogen: pin v1.2.0 release commit From 4e25ce9e9ffd0c9e285120b0c76c4816a44c8ec5 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:03:16 +0000 Subject: [PATCH 112/262] autogen(docs): generate and bump docs [skip ci] --- quickstart.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quickstart.yml b/quickstart.yml index 68ebaa60185b..10a331c397b1 100644 --- a/quickstart.yml +++ b/quickstart.yml @@ -1,7 +1,7 @@ version: '3.7' services: kratos-migrate: - image: oryd/kratos:v1.1.0 + image: oryd/kratos:v1.2.0 environment: - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc volumes: @@ -17,7 +17,7 @@ services: networks: - intranet kratos-selfservice-ui-node: - image: oryd/kratos-selfservice-ui-node:v1.1.0 + image: oryd/kratos-selfservice-ui-node:v1.2.0 environment: - KRATOS_PUBLIC_URL=http://kratos:4433/ - KRATOS_BROWSER_URL=http://127.0.0.1:4433/ @@ -30,7 +30,7 @@ services: kratos: depends_on: - kratos-migrate - image: oryd/kratos:v1.1.0 + image: oryd/kratos:v1.2.0 ports: - '4433:4433' # public - '4434:4434' # admin From ba0f30d56ec2869fff6e6b7f4e3e093618f38b5a Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:03:34 +0000 Subject: [PATCH 113/262] autogen: add v1.2.0 to version.schema.json [skip ci] --- .schema/version.schema.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.schema/version.schema.json b/.schema/version.schema.json index 1676b52a06a5..14192708b3c7 100644 --- a/.schema/version.schema.json +++ b/.schema/version.schema.json @@ -2,6 +2,23 @@ "$id": "https://github.com/ory/kratos/.schema/versions.config.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "oneOf": [ + { + "allOf": [ + { + "properties": { + "version": { + "const": "v1.2.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v1.2.0/.schemastore/config.schema.json" + } + ] + }, { "allOf": [ { From 0213ed90532832d480f2e8ce9fa602d409a66224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20B=C5=82aszczyk?= Date: Wed, 5 Jun 2024 13:12:15 +0200 Subject: [PATCH 114/262] chore: add kubescape image scanner (#3947) --- .docker/Dockerfile-alpine | 2 +- .github/workflows/cve-scan.yaml | 9 +++++++++ go.mod | 4 ++-- go.sum | 8 ++++---- internal/client-go/go.sum | 1 + 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.docker/Dockerfile-alpine b/.docker/Dockerfile-alpine index 6f1713e00bcf..6e1848e228ed 100644 --- a/.docker/Dockerfile-alpine +++ b/.docker/Dockerfile-alpine @@ -1,4 +1,4 @@ -FROM alpine:3.18.3 +FROM alpine:3.20.0 # Because this image supports SQLite, we create /home/ory and /home/ory/sqlite which is owned by the ory user # and declare /home/ory/sqlite a volume. diff --git a/.github/workflows/cve-scan.yaml b/.github/workflows/cve-scan.yaml index 8943b520bf85..5b4519f66488 100644 --- a/.github/workflows/cve-scan.yaml +++ b/.github/workflows/cve-scan.yaml @@ -48,6 +48,15 @@ jobs: uses: github/codeql-action/upload-sarif@v2 with: sarif_file: ${{ steps.grype-scan.outputs.sarif }} + - name: Kubescape scanner + uses: kubescape/github-action@main + id: kubescape + with: + image: oryd/kratos:${{ env.SHA_SHORT }} + verbose: true + format: pretty-printer + # can't whitelist CVE yet: https://github.com/kubescape/kubescape/pull/1568 + severityThreshold: critical - name: Trivy Scanner uses: aquasecurity/trivy-action@master if: ${{ always() }} diff --git a/go.mod b/go.mod index b15b91816bf4..537942ebacbd 100644 --- a/go.mod +++ b/go.mod @@ -331,12 +331,12 @@ require ( require ( github.com/coreos/go-oidc/v3 v3.9.0 github.com/dghubble/oauth1 v0.7.2 - github.com/lestrrat-go/jwx/v2 v2.0.19 + github.com/lestrrat-go/jwx/v2 v2.0.21 ) require ( github.com/jackc/puddle/v2 v2.1.2 // indirect - github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/httprc v1.0.5 // indirect github.com/segmentio/asm v1.2.0 // indirect go.uber.org/atomic v1.10.0 // indirect ) diff --git a/go.sum b/go.sum index 864128132731..85e85e83626c 100644 --- a/go.sum +++ b/go.sum @@ -674,14 +674,14 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= -github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk= +github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= -github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= -github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= +github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0= +github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 278d8e089e78e481274d1a18578cc60077e94200 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:13:45 +0000 Subject: [PATCH 115/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 4e3fad4b4739b5cf00d658155350cb599f2cd06a Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:37:39 +0200 Subject: [PATCH 116/262] feat: improve session extend performance (#3948) This patch improves the performance for extending session lifespans. Lifespan extension is tricky as it is often part of the middleware of Ory Kratos consumers. As such, it is prone to transaction contention when we read and write to the same session row at the same time (and potentially multiple times). To address this, we: 1. Introduce a locking mechanism on the row to reduce transaction contention; 2. Add a new feature flag that toggles returning 204 no content instead of 200 + session. Be aware that all reads on the session table will have to wait for the transaction to commit before they return a value. This may cause long(er) response times on `/session/whoami` for sessions that are being extended at the same time. BREAKING CHANGES: Going forward, the `/admin/session/.../extend` endpoint will return 204 no content for new Ory Network projects. We will deprecate returning 200 + session body in the future. --- driver/config/config.go | 5 ++ embedx/config.schema.json | 6 ++ persistence/sql/persister_session.go | 59 ++++++++++++++++++++ session/handler.go | 27 ++++++--- session/persistence.go | 3 + session/test/persistence.go | 82 ++++++++++++++++++++++++++++ x/events/events.go | 58 +++++++++++++------- 7 files changed, 211 insertions(+), 29 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index 0d755a11ba63..05d7ddef52a7 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -116,6 +116,7 @@ const ( ViperKeySessionTokenizerTemplates = "session.whoami.tokenizer.templates" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" ViperKeySessionWhoAmICaching = "feature_flags.cacheable_sessions" + ViperKeyFeatureFlagFasterSessionExtend = "feature_flags.faster_session_extend" ViperKeySessionWhoAmICachingMaxAge = "feature_flags.cacheable_sessions_max_age" ViperKeyUseContinueWithTransitions = "feature_flags.use_continue_with_transitions" ViperKeySessionRefreshMinTimeLeft = "session.earliest_possible_extend" @@ -1369,6 +1370,10 @@ func (p *Config) SessionWhoAmICaching(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeySessionWhoAmICaching) } +func (p *Config) FeatureFlagFasterSessionExtend(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyFeatureFlagFasterSessionExtend) +} + func (p *Config) SessionWhoAmICachingMaxAge(ctx context.Context) time.Duration { return p.GetProvider(ctx).DurationF(ViperKeySessionWhoAmICachingMaxAge, 0) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 79bc83c88b1d..5349164b6f9b 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2852,6 +2852,12 @@ "title": "Enable new flow transitions using `continue_with` items", "description": "If enabled allows new flow transitions using `continue_with` items.", "default": false + }, + "faster_session_extend": { + "type": "boolean", + "title": "Enable faster session extension", + "description": "If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future.", + "default": false } }, "additionalProperties": false diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go index c0c1c3d865ba..412ed2d8a825 100644 --- a/persistence/sql/persister_session.go +++ b/persistence/sql/persister_session.go @@ -8,6 +8,9 @@ import ( "fmt" "time" + "github.com/ory/herodot" + "github.com/ory/x/dbal" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/pkg/errors" @@ -176,6 +179,61 @@ func (p *Persister) ListSessionsByIdentity( return s, t, nil } +// ExtendSession updates the expiry of a session. +func (p *Persister) ExtendSession(ctx context.Context, sessionID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ExtendSession") + defer otelx.End(span, &err) + + nid := p.NetworkID(ctx) + s := new(session.Session) + var didRefresh bool + if err := errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { + lockBehavior := "" + if tx.Dialect.Name() == dbal.DriverCockroachDB { + // SKIP LOCKED returns no rows if the row is locked by another transaction. + lockBehavior = "FOR UPDATE SKIP LOCKED" + } + + if err := tx. + Where( + // We make use of the fact that CRDB supports FOR UPDATE as part of the WHERE clause. + fmt.Sprintf("id = ? AND nid = ? %s", lockBehavior), + sessionID, nid, + ).First(s); err != nil { + + // This is a special case for CockroachDB. If the row is locked, we do not see the session. Therefor we return + // a 404 not found error indicating to the user that the session might already be updated by someone else. + if errors.Is(err, sqlcon.ErrNoRows) && tx.Dialect.Name() == dbal.DriverCockroachDB { + return errors.WithStack(herodot.ErrNotFound.WithReason("The session you are trying to extend is already being extended by another request or does not exist.")) + } + + return sqlcon.HandleError(err) + } + + if !s.CanBeRefreshed(ctx, p.r.Config()) { + // This prevents excessive writes to the database. + return nil + } + + didRefresh = true + s = s.Refresh(ctx, p.r.Config()) + + if _, err := tx.Where("id = ? AND nid = ?", sessionID, nid).UpdateQuery(s, "expires_at"); err != nil { + return sqlcon.HandleError(err) + } + + return nil + })); err != nil { + return err + } + + if didRefresh { + trace.SpanFromContext(ctx).AddEvent(events.NewSessionLifespanExtended(ctx, s.ID, s.IdentityID, s.ExpiresAt)) + } + + return nil +} + // UpsertSession creates a session if not found else updates. // This operation also inserts Session device records when a session is being created. // The update operation skips updating Session device records since only one record would need to be updated in this case. @@ -196,6 +254,7 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err trace.SpanFromContext(ctx).AddEvent(events.NewSessionIssued(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) } }() + return errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { updated = false exists := false diff --git a/session/handler.go b/session/handler.go index 8953fb37c6c3..9dab8860c773 100644 --- a/session/handler.go +++ b/session/handler.go @@ -873,6 +873,10 @@ type extendSession struct { // Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it // will only extend the session after the specified time has passed. // +// This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may +// return a 200 OK response with the session in the body. Returning the session as part of the response +// will be deprecated in the future and should not be relied upon. +// // Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. // // Schemes: http, https @@ -882,30 +886,35 @@ type extendSession struct { // // Responses: // 200: session +// 204: emptyResponse // 400: errorGeneric // 404: errorGeneric // default: errorGeneric func (h *Handler) adminSessionExtend(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - iID, err := uuid.FromString(ps.ByName("id")) + id, err := uuid.FromString(ps.ByName("id")) if err != nil { h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithError(err.Error()).WithDebug("could not parse UUID"))) return } - s, err := h.r.SessionPersister().GetSession(r.Context(), iID, ExpandDefault) - if err != nil { + c := h.r.Config() + if err := h.r.SessionPersister().ExtendSession(r.Context(), id); err != nil { h.r.Writer().WriteError(w, r, err) return } - c := h.r.Config() - if s.CanBeRefreshed(r.Context(), c) { - if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(r.Context(), c)); err != nil { - h.r.Writer().WriteError(w, r, err) - return - } + // Default behavior going forward. + if c.FeatureFlagFasterSessionExtend(r.Context()) { + w.WriteHeader(http.StatusNoContent) + return } + // WARNING - this will be deprecated at some point! + s, err := h.r.SessionPersister().GetSession(r.Context(), id, ExpandDefault) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } h.r.Writer().Write(w, r, s) } diff --git a/session/persistence.go b/session/persistence.go index 34bf4d2654d4..ab5ec0a47735 100644 --- a/session/persistence.go +++ b/session/persistence.go @@ -35,6 +35,9 @@ type Persister interface { // UpsertSession inserts or updates a session into / in the store. UpsertSession(ctx context.Context, s *Session) error + // ExtendSession updates the expiry of a session. + ExtendSession(ctx context.Context, sessionID uuid.UUID) error + // DeleteSession removes a session from the store. DeleteSession(ctx context.Context, id uuid.UUID) error diff --git a/session/test/persistence.go b/session/test/persistence.go index 727cc744bdce..8e8cbfeb18b2 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -8,6 +8,11 @@ import ( "testing" "time" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + + "github.com/ory/x/dbal" + "github.com/gobuffalo/pop/v6" "github.com/ory/x/pagination/keysetpagination" @@ -604,5 +609,82 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { _, err = p.GetSessionByToken(ctx, t2, session.ExpandNothing, identity.ExpandDefault) require.ErrorIs(t, err, sqlcon.ErrNoRows) }) + + t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) + }) + + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.ExpiresAt = time.Now().Add(time.Hour * 10).Round(time.Second).UTC() + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + require.NoError(t, p.ExtendSession(ctx, expected.ID)) + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, expected.ExpiresAt, actual.ExpiresAt) + }) + + t.Run("extend session lifespan", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) + }) + + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.ExpiresAt = time.Now().Add(time.Hour).UTC() + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute) + require.NoError(t, p.ExtendSession(ctx, expected.ID)) + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute)) + }) + + t.Run("extend session lifespan on CockroachDB", func(t *testing.T) { + if p.GetConnection(ctx).Dialect.Name() != dbal.DriverCockroachDB { + t.Skip("Skipping test because driver is not CockroachDB") + } + + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) + }) + + conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.ExpiresAt = time.Now().Add(time.Hour).UTC() + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute) + + var foundExpectedCockroachError bool + g := errgroup.Group{} + for i := 0; i < 10; i++ { + g.Go(func() error { + err := p.ExtendSession(ctx, expected.ID) + if errors.Is(err, sqlcon.ErrNoRows) { + foundExpectedCockroachError = true + return nil + } + return err + }) + } + require.NoError(t, g.Wait()) + + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute)) + assert.True(t, foundExpectedCockroachError, "We expect to find a not found error caused by ... FOR UPDATE SKIP LOCKED") + }) } } diff --git a/x/events/events.go b/x/events/events.go index 52e921112d12..13ec16955539 100644 --- a/x/events/events.go +++ b/x/events/events.go @@ -16,31 +16,33 @@ import ( ) const ( - SessionIssued semconv.Event = "SessionIssued" - SessionChanged semconv.Event = "SessionChanged" - SessionRevoked semconv.Event = "SessionRevoked" - SessionChecked semconv.Event = "SessionChecked" - SessionTokenizedAsJWT semconv.Event = "SessionTokenizedAsJWT" - RegistrationFailed semconv.Event = "RegistrationFailed" - RegistrationSucceeded semconv.Event = "RegistrationSucceeded" - LoginFailed semconv.Event = "LoginFailed" - LoginSucceeded semconv.Event = "LoginSucceeded" - SettingsFailed semconv.Event = "SettingsFailed" - SettingsSucceeded semconv.Event = "SettingsSucceeded" - RecoveryFailed semconv.Event = "RecoveryFailed" - RecoverySucceeded semconv.Event = "RecoverySucceeded" - VerificationFailed semconv.Event = "VerificationFailed" - VerificationSucceeded semconv.Event = "VerificationSucceeded" - IdentityCreated semconv.Event = "IdentityCreated" - IdentityUpdated semconv.Event = "IdentityUpdated" - WebhookDelivered semconv.Event = "WebhookDelivered" - WebhookSucceeded semconv.Event = "WebhookSucceeded" - WebhookFailed semconv.Event = "WebhookFailed" + SessionIssued semconv.Event = "SessionIssued" + SessionChanged semconv.Event = "SessionChanged" + SessionLifespanExtended semconv.Event = "SessionLifespanExtended" + SessionRevoked semconv.Event = "SessionRevoked" + SessionChecked semconv.Event = "SessionChecked" + SessionTokenizedAsJWT semconv.Event = "SessionTokenizedAsJWT" + RegistrationFailed semconv.Event = "RegistrationFailed" + RegistrationSucceeded semconv.Event = "RegistrationSucceeded" + LoginFailed semconv.Event = "LoginFailed" + LoginSucceeded semconv.Event = "LoginSucceeded" + SettingsFailed semconv.Event = "SettingsFailed" + SettingsSucceeded semconv.Event = "SettingsSucceeded" + RecoveryFailed semconv.Event = "RecoveryFailed" + RecoverySucceeded semconv.Event = "RecoverySucceeded" + VerificationFailed semconv.Event = "VerificationFailed" + VerificationSucceeded semconv.Event = "VerificationSucceeded" + IdentityCreated semconv.Event = "IdentityCreated" + IdentityUpdated semconv.Event = "IdentityUpdated" + WebhookDelivered semconv.Event = "WebhookDelivered" + WebhookSucceeded semconv.Event = "WebhookSucceeded" + WebhookFailed semconv.Event = "WebhookFailed" ) const ( attributeKeySessionID semconv.AttributeKey = "SessionID" attributeKeySessionAAL semconv.AttributeKey = "SessionAAL" + attributeKeySessionExpiresAt semconv.AttributeKey = "SessionExpiresAt" attributeKeySelfServiceFlowType semconv.AttributeKey = "SelfServiceFlowType" attributeKeySelfServiceMethodUsed semconv.AttributeKey = "SelfServiceMethodUsed" attributeKeySelfServiceSSOProviderUsed semconv.AttributeKey = "SelfServiceSSOProviderUsed" @@ -71,6 +73,10 @@ func attLoginRequestedAAL(val string) otelattr.KeyValue { return otelattr.String(attributeKeyLoginRequestedAAL.String(), val) } +func attSessionExpiresAt(expiresAt time.Time) otelattr.KeyValue { + return otelattr.String(attributeKeySessionExpiresAt.String(), expiresAt.String()) +} + func attLoginRequestedPrivilegedSession(val bool) otelattr.KeyValue { return otelattr.Bool(attributeKeyLoginRequestedPrivilegedSession.String(), val) } @@ -135,6 +141,18 @@ func NewSessionChanged(ctx context.Context, aal string, sessionID, identityID uu ) } +func NewSessionLifespanExtended(ctx context.Context, sessionID, identityID uuid.UUID, newExpiry time.Time) (string, trace.EventOption) { + return SessionLifespanExtended.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + attSessionExpiresAt(newExpiry), + )..., + ) +} + type LoginSucceededOpts struct { SessionID, IdentityID uuid.UUID FlowType, RequestedAAL, Method, SSOProvider string From 369aad447b9b668b596d9645ee48c24f6e41429c Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:39:11 +0000 Subject: [PATCH 117/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/api_identity.go | 8 ++++++++ internal/httpclient/api_identity.go | 8 ++++++++ spec/api.json | 5 ++++- spec/swagger.json | 5 ++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index 04774a5b3938..62f5f80b36c2 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -157,6 +157,10 @@ type IdentityApi interface { * Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it will only extend the session after the specified time has passed. + This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may + return a 200 OK response with the session in the body. Returning the session as part of the response + will be deprecated in the future and should not be relied upon. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. @@ -1486,6 +1490,10 @@ func (r IdentityApiApiExtendSessionRequest) Execute() (*Session, *http.Response, will only extend the session after the specified time has passed. +This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may +return a 200 OK response with the session in the body. Returning the session as part of the response +will be deprecated in the future and should not be relied upon. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index 04774a5b3938..62f5f80b36c2 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -157,6 +157,10 @@ type IdentityApi interface { * Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it will only extend the session after the specified time has passed. + This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may + return a 200 OK response with the session in the body. Returning the session as part of the response + will be deprecated in the future and should not be relied upon. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. @@ -1486,6 +1490,10 @@ func (r IdentityApiApiExtendSessionRequest) Execute() (*Session, *http.Response, will only extend the session after the specified time has passed. +This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may +return a 200 OK response with the session in the body. Returning the session as part of the response +will be deprecated in the future and should not be relied upon. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. diff --git a/spec/api.json b/spec/api.json index 365cc3fc5f7f..084bbfa5ea47 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4969,7 +4969,7 @@ }, "/admin/sessions/{id}/extend": { "patch": { - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "operationId": "extendSession", "parameters": [ { @@ -4993,6 +4993,9 @@ }, "description": "session" }, + "204": { + "$ref": "#/components/responses/emptyResponse" + }, "400": { "content": { "application/json": { diff --git a/spec/swagger.json b/spec/swagger.json index 9753c89c8991..8ccd39801919 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1188,7 +1188,7 @@ "oryAccessToken": [] } ], - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "schemes": [ "http", "https" @@ -1214,6 +1214,9 @@ "$ref": "#/definitions/session" } }, + "204": { + "$ref": "#/responses/emptyResponse" + }, "400": { "description": "errorGeneric", "schema": { From 1bc4dc5b24c72e26c2b1022edee0312a357bea8d Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:12:53 +0200 Subject: [PATCH 118/262] chore: move b2b config to selfservice section (#3949) --- embedx/config.schema.json | 661 ++++++++++++++++++++++++++++++-------- internal/client-go/go.sum | 1 + 2 files changed, 527 insertions(+), 135 deletions(-) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 5349164b6f9b..5ebbdb1241ee 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -43,7 +43,10 @@ "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/dashboard", "/dashboard"] + "examples": [ + "https://my-app.com/dashboard", + "/dashboard" + ] }, "selfServiceSessionRevokerHook": { "type": "object", @@ -53,7 +56,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceSessionIssuerHook": { "type": "object", @@ -63,7 +68,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceRequireVerifiedAddressHook": { "type": "object", @@ -73,7 +80,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceVerificationHook": { "type": "object", @@ -83,7 +92,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceShowVerificationUIHook": { "type": "object", @@ -93,7 +104,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "b2bSSOHook": { "type": "object", @@ -107,7 +120,10 @@ } }, "additionalProperties": false, - "required": ["hook", "config"] + "required": [ + "hook", + "config" + ] }, "webHookAuthBasicAuthProperties": { "properties": { @@ -127,11 +143,17 @@ } }, "additionalProperties": false, - "required": ["user", "password"] + "required": [ + "user", + "password" + ] } }, "additionalProperties": false, - "required": ["type", "config"] + "required": [ + "type", + "config" + ] }, "httpRequestConfig": { "type": "object", @@ -139,7 +161,9 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to send the emails to.", - "examples": ["https://example.com/api/v1/email"], + "examples": [ + "https://example.com/api/v1/email" + ], "type": "string", "pattern": "^https?://" }, @@ -204,15 +228,25 @@ "in": { "type": "string", "description": "How the api key should be transferred", - "enum": ["header", "cookie"] + "enum": [ + "header", + "cookie" + ] } }, "additionalProperties": false, - "required": ["name", "value", "in"] + "required": [ + "name", + "value", + "in" + ] } }, "additionalProperties": false, - "required": ["type", "config"] + "required": [ + "type", + "config" + ] }, "selfServiceWebHook": { "type": "object", @@ -251,7 +285,10 @@ "const": true } }, - "required": ["ignore", "parse"] + "required": [ + "ignore", + "parse" + ] } }, "url": { @@ -324,30 +361,46 @@ "response": { "properties": { "ignore": { - "enum": [true] + "enum": [ + true + ] } }, - "required": ["ignore"] + "required": [ + "ignore" + ] } }, - "required": ["response"] + "required": [ + "response" + ] } }, { "properties": { "can_interrupt": { - "enum": [false] + "enum": [ + false + ] } }, - "require": ["can_interrupt"] + "require": [ + "can_interrupt" + ] } ], "additionalProperties": false, - "required": ["url", "method"] + "required": [ + "url", + "method" + ] } }, "additionalProperties": false, - "required": ["hook", "config"] + "required": [ + "hook", + "config" + ] }, "OIDCClaims": { "title": "OpenID Connect claims", @@ -380,7 +433,9 @@ "essential": true }, "acr": { - "values": ["urn:mace:incommon:iap:silver"] + "values": [ + "urn:mace:incommon:iap:silver" + ] } } } @@ -428,7 +483,9 @@ "properties": { "id": { "type": "string", - "examples": ["google"] + "examples": [ + "google" + ] }, "provider": { "title": "Provider", @@ -457,7 +514,9 @@ "lark", "x" ], - "examples": ["google"] + "examples": [ + "google" + ] }, "label": { "title": "Optional string which will be used when generating labels for UI buttons.", @@ -472,17 +531,23 @@ "issuer_url": { "type": "string", "format": "uri", - "examples": ["https://accounts.google.com"] + "examples": [ + "https://accounts.google.com" + ] }, "auth_url": { "type": "string", "format": "uri", - "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + "examples": [ + "https://accounts.google.com/o/oauth2/v2/auth" + ] }, "token_url": { "type": "string", "format": "uri", - "examples": ["https://www.googleapis.com/oauth2/v4/token"] + "examples": [ + "https://www.googleapis.com/oauth2/v4/token" + ] }, "mapper_url": { "title": "Jsonnet Mapper URL", @@ -499,7 +564,10 @@ "type": "array", "items": { "type": "string", - "examples": ["offline_access", "profile"] + "examples": [ + "offline_access", + "profile" + ] } }, "microsoft_tenant": { @@ -518,21 +586,30 @@ "title": "Microsoft subject source", "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier.", "type": "string", - "enum": ["userinfo", "me"], + "enum": [ + "userinfo", + "me" + ], "default": "userinfo", - "examples": ["userinfo"] + "examples": [ + "userinfo" + ] }, "apple_team_id": { "title": "Apple Developer Team ID", "description": "Apple Developer Team ID needed for generating a JWT token for client secret", "type": "string", - "examples": ["KP76DQS54M"] + "examples": [ + "KP76DQS54M" + ] }, "apple_private_key_id": { "title": "Apple Private Key Identifier", "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", "type": "string", - "examples": ["UX56C66723"] + "examples": [ + "UX56C66723" + ] }, "apple_private_key": { "title": "Apple Private Key", @@ -549,27 +626,42 @@ "title": "Organization ID", "description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.", "type": "string", - "examples": ["12345678-1234-1234-1234-123456789012"] + "examples": [ + "12345678-1234-1234-1234-123456789012" + ] }, "additional_id_token_audiences": { "title": "Additional client ids allowed when using ID token submission", "type": "array", "items": { "type": "string", - "examples": ["12345678-1234-1234-1234-123456789012"] + "examples": [ + "12345678-1234-1234-1234-123456789012" + ] } }, "claims_source": { "title": "Claims source", "description": "Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`", "type": "string", - "enum": ["id_token", "userinfo"], + "enum": [ + "id_token", + "userinfo" + ], "default": "id_token", - "examples": ["id_token", "userinfo"] + "examples": [ + "id_token", + "userinfo" + ] } }, "additionalProperties": false, - "required": ["id", "provider", "client_id", "mapper_url"], + "required": [ + "id", + "provider", + "client_id", + "mapper_url" + ], "allOf": [ { "if": { @@ -578,17 +670,23 @@ "const": "microsoft" } }, - "required": ["provider"] + "required": [ + "provider" + ] }, "then": { - "required": ["microsoft_tenant"] + "required": [ + "microsoft_tenant" + ] }, "else": { "not": { "properties": { "microsoft_tenant": {} }, - "required": ["microsoft_tenant"] + "required": [ + "microsoft_tenant" + ] } } }, @@ -599,7 +697,9 @@ "const": "apple" } }, - "required": ["provider"] + "required": [ + "provider" + ] }, "then": { "not": { @@ -609,7 +709,9 @@ "minLength": 1 } }, - "required": ["client_secret"] + "required": [ + "client_secret" + ] }, "required": [ "apple_private_key_id", @@ -618,7 +720,9 @@ ] }, "else": { - "required": ["client_secret"], + "required": [ + "client_secret" + ], "allOf": [ { "not": { @@ -628,7 +732,9 @@ "minLength": 1 } }, - "required": ["apple_team_id"] + "required": [ + "apple_team_id" + ] } }, { @@ -639,7 +745,9 @@ "minLength": 1 } }, - "required": ["apple_private_key_id"] + "required": [ + "apple_private_key_id" + ] } }, { @@ -650,7 +758,9 @@ "minLength": 1 } }, - "required": ["apple_private_key"] + "required": [ + "apple_private_key" + ] } } ] @@ -830,7 +940,10 @@ "title": "Required Authenticator Assurance Level", "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.", "type": "string", - "enum": ["aal1", "highest_available"], + "enum": [ + "aal1", + "highest_available" + ], "default": "highest_available" }, "selfServiceAfterSettings": { @@ -1026,7 +1139,9 @@ "path": { "title": "Path to PEM-encoded Fle", "type": "string", - "examples": ["path/to/file.pem"] + "examples": [ + "path/to/file.pem" + ] }, "base64": { "title": "Base64 Encoded Inline", @@ -1074,7 +1189,9 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] }, "valid": { "additionalProperties": false, @@ -1087,7 +1204,9 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] } } }, @@ -1158,7 +1277,9 @@ "selfservice": { "type": "object", "additionalProperties": false, - "required": ["default_browser_return_url"], + "required": [ + "default_browser_return_url" + ], "properties": { "default_browser_return_url": { "$ref": "#/definitions/defaultReturnTo" @@ -1193,20 +1314,30 @@ "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/user/settings"], + "examples": [ + "https://my-app.com/user/settings" + ], "default": "https://www.ory.sh/kratos/docs/fallback/settings" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "privileged_session_max_age": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "required_aal": { "$ref": "#/definitions/featureRequiredAal" @@ -1255,14 +1386,20 @@ "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/signup"], + "examples": [ + "https://my-app.com/signup" + ], "default": "https://www.ory.sh/kratos/docs/fallback/registration" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "before": { "$ref": "#/definitions/selfServiceBeforeRegistration" @@ -1287,14 +1424,30 @@ "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/login"], + "examples": [ + "https://my-app.com/login" + ], "default": "https://www.ory.sh/kratos/docs/fallback/login" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] + }, + "style": { + "title": "Login Flow Style", + "description": "The style of the login flow. If set to `one_step` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.", + "type": "string", + "enum": [ + "one_step", + "identifier_first" + ], + "default": "one_step" }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" @@ -1320,7 +1473,9 @@ "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/verify"], + "examples": [ + "https://my-app.com/verify" + ], "default": "https://www.ory.sh/kratos/docs/fallback/verification" }, "after": { @@ -1332,7 +1487,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "before": { "$ref": "#/definitions/selfServiceBeforeVerification" @@ -1341,7 +1500,10 @@ "title": "Verification Strategy", "description": "The strategy to use for verification requests", "type": "string", - "enum": ["link", "code"], + "enum": [ + "link", + "code" + ], "default": "code" }, "notify_unknown_recipients": { @@ -1368,7 +1530,9 @@ "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/verify"], + "examples": [ + "https://my-app.com/verify" + ], "default": "https://www.ory.sh/kratos/docs/fallback/recovery" }, "after": { @@ -1380,7 +1544,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "before": { "$ref": "#/definitions/selfServiceBeforeRecovery" @@ -1389,7 +1557,10 @@ "title": "Recovery Strategy", "description": "The strategy to use for recovery requests", "type": "string", - "enum": ["link", "code"], + "enum": [ + "link", + "code" + ], "default": "code" }, "notify_unknown_recipients": { @@ -1409,7 +1580,9 @@ "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/kratos-error"], + "examples": [ + "https://my-app.com/kratos-error" + ], "default": "https://www.ory.sh/kratos/docs/fallback/error" } } @@ -1420,6 +1593,54 @@ "type": "object", "additionalProperties": false, "properties": { + "b2b": { + "title": "Single Sign-On for B2B", + "description": "Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code.", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "organizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the organization.", + "format": "uuid", + "examples": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "label": { + "type": "string", + "description": "The label of the organization.", + "examples": [ + "ACME SSO" + ] + }, + "domains": { + "type": "array", + "items": { + "type": "string", + "format": "hostname", + "examples": [ + "my-app.com" + ], + "description": "If this domain matches the email's domain, this provider is shown." + } + } + } + } + } + } + } + }, + "additionalProperties": false + }, "profile": { "type": "object", "additionalProperties": false, @@ -1448,14 +1669,20 @@ "base_url": { "title": "Override the base URL which should be used as the base for recovery and verification links.", "type": "string", - "examples": ["https://my-app.com"] + "examples": [ + "https://my-app.com" + ] }, "lifespan": { "title": "How long a link is valid for", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } } @@ -1463,24 +1690,36 @@ }, "code": { "type": "object", - "additionalProperties": false, + "additionalProperties": true, "anyOf": [ { "properties": { - "passwordless_enabled": { "const": true }, - "mfa_enabled": { "const": false } + "passwordless_enabled": { + "const": true + }, + "mfa_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": true }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": true + }, + "passwordless_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": false }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": false + }, + "passwordless_enabled": { + "const": false + } } } ], @@ -1517,7 +1756,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } } @@ -1640,13 +1883,17 @@ "type": "string", "title": "Relying Party Display Name", "description": "An name to help the user identify this RP.", - "examples": ["Ory Foundation"] + "examples": [ + "Ory Foundation" + ] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": ["ory.sh"] + "examples": [ + "ory.sh" + ] }, "origin": { "type": "string", @@ -1654,7 +1901,9 @@ "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).", "format": "uri", "deprecationMessage": "This field is deprecated. Use `origins` instead.", - "examples": ["https://www.ory.sh"] + "examples": [ + "https://www.ory.sh" + ] }, "origins": { "type": "array", @@ -1675,13 +1924,18 @@ "description": "An icon to help the user identify this RP.", "format": "uri", "deprecationMessage": "This field is deprecated and ignored due to security considerations.", - "examples": ["https://www.ory.sh/an-icon.png"] + "examples": [ + "https://www.ory.sh/an-icon.png" + ] } }, "type": "object", "oneOf": [ { - "required": ["id", "display_name"], + "required": [ + "id", + "display_name" + ], "properties": { "origin": { "not": {} @@ -1692,7 +1946,11 @@ } }, { - "required": ["id", "display_name", "origin"], + "required": [ + "id", + "display_name", + "origin" + ], "properties": { "origin": { "type": "string" @@ -1703,7 +1961,11 @@ } }, { - "required": ["id", "display_name", "origins"], + "required": [ + "id", + "display_name", + "origins" + ], "properties": { "origin": { "not": {} @@ -1728,10 +1990,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "then": { - "required": ["config"] + "required": [ + "config" + ] } }, "passkey": { @@ -1754,13 +2020,17 @@ "type": "string", "title": "Relying Party Display Name", "description": "A name to help the user identify this RP.", - "examples": ["Ory Foundation"] + "examples": [ + "Ory Foundation" + ] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": ["ory.sh"] + "examples": [ + "ory.sh" + ] }, "origins": { "type": "array", @@ -1777,7 +2047,10 @@ } }, "type": "object", - "required": ["display_name", "id"] + "required": [ + "display_name", + "id" + ] } }, "additionalProperties": false @@ -1789,10 +2062,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "then": { - "required": ["config"] + "required": [ + "config" + ] } }, "oidc": { @@ -1815,7 +2092,9 @@ "title": "Base URL for OAuth2 Redirect URIs", "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.", "format": "uri", - "examples": ["https://auth.myexample.org/"] + "examples": [ + "https://auth.myexample.org/" + ] }, "providers": { "title": "OpenID Connect and OAuth2 Providers", @@ -1920,7 +2199,9 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] } } }, @@ -1939,7 +2220,9 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] } } } @@ -1949,13 +2232,18 @@ "type": "string", "title": "Override message templates", "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", - "examples": ["/conf/courier-templates"] + "examples": [ + "/conf/courier-templates" + ] }, "message_retries": { "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned", "type": "integer", "default": 5, - "examples": [10, 60] + "examples": [ + 10, + 60 + ] }, "worker": { "description": "Configures the dispatch worker.", @@ -1978,7 +2266,10 @@ "title": "Delivery Strategy", "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", "type": "string", - "enum": ["smtp", "http"], + "enum": [ + "smtp", + "http" + ], "default": "smtp" }, "http": { @@ -2035,7 +2326,9 @@ "title": "SMTP Sender Name", "description": "The recipient of an email will see this as the sender name.", "type": "string", - "examples": ["Bob"] + "examples": [ + "Bob" + ] }, "headers": { "title": "SMTP Headers", @@ -2083,7 +2376,9 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to connect to the SMS provider.", - "examples": ["https://api.twillio.com/sms/send"], + "examples": [ + "https://api.twillio.com/sms/send" + ], "type": "string", "pattern": "^https?:\\/\\/.*" }, @@ -2125,7 +2420,10 @@ }, "additionalProperties": false }, - "required": ["url", "method"], + "required": [ + "url", + "method" + ], "additionalProperties": false } }, @@ -2142,19 +2440,26 @@ "title": "Channel id", "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only phone is supported.", "maxLength": 32, - "enum": ["sms"] + "enum": [ + "sms" + ] }, "type": { "type": "string", "title": "Channel type", "description": "The channel type. Currently only http is supported.", - "enum": ["http"] + "enum": [ + "http" + ] }, "request_config": { "$ref": "#/definitions/httpRequestConfig" } }, - "required": ["id", "request_config"], + "required": [ + "id", + "request_config" + ], "additionalProperties": false } } @@ -2205,7 +2510,10 @@ "type": "string", "title": "Default Read Consistency Level", "description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.", - "enum": ["strong", "eventual"], + "enum": [ + "strong", + "eventual" + ], "default": "strong" } } @@ -2233,7 +2541,9 @@ "description": "The URL where the admin endpoint is exposed at.", "type": "string", "format": "uri", - "examples": ["https://kratos.private-network:4434/"] + "examples": [ + "https://kratos.private-network:4434/" + ] }, "host": { "title": "Admin Host", @@ -2247,7 +2557,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4434], + "examples": [ + 4434 + ], "default": 4434 }, "socket": { @@ -2306,7 +2618,9 @@ ] }, "uniqueItems": true, - "default": ["*"], + "default": [ + "*" + ], "examples": [ [ "https://example.com", @@ -2318,7 +2632,13 @@ "allowed_methods": { "type": "array", "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", - "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "default": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE" + ], "items": { "type": "string", "enum": [ @@ -2352,7 +2672,9 @@ "exposed_headers": { "type": "array", "description": "Sets which headers are safe to expose to the API of a CORS API specification.", - "default": ["Content-Type"], + "default": [ + "Content-Type" + ], "items": { "type": "string" } @@ -2395,7 +2717,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4433], + "examples": [ + 4433 + ], "default": 4433 }, "socket": { @@ -2445,7 +2769,10 @@ "format": { "description": "The log format can either be text or JSON.", "type": "string", - "enum": ["json", "text"] + "enum": [ + "json", + "text" + ] } }, "additionalProperties": false @@ -2486,7 +2813,9 @@ "id": { "title": "The schema's ID.", "type": "string", - "examples": ["employee"] + "examples": [ + "employee" + ] }, "url": { "type": "string", @@ -2500,11 +2829,16 @@ ] } }, - "required": ["id", "url"] + "required": [ + "id", + "url" + ] } } }, - "required": ["schemas"], + "required": [ + "schemas" + ], "additionalProperties": false }, "secrets": { @@ -2553,7 +2887,10 @@ "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.", "type": "string", "default": "bcrypt", - "enum": ["argon2", "bcrypt"] + "enum": [ + "argon2", + "bcrypt" + ] }, "argon2": { "title": "Configuration for the Argon2id hasher.", @@ -2609,7 +2946,9 @@ "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.", "type": "object", "additionalProperties": false, - "required": ["cost"], + "required": [ + "cost" + ], "properties": { "cost": { "type": "integer", @@ -2631,7 +2970,11 @@ "description": "One of the values: noop, aes, xchacha20-poly1305", "type": "string", "default": "noop", - "enum": ["noop", "aes", "xchacha20-poly1305"] + "enum": [ + "noop", + "aes", + "xchacha20-poly1305" + ] } } }, @@ -2655,7 +2998,11 @@ "title": "HTTP Cookie Same Site Configuration", "description": "Sets the session and CSRF cookie SameSite.", "type": "string", - "enum": ["Strict", "Lax", "None"], + "enum": [ + "Strict", + "Lax", + "None" + ], "default": "Lax" } }, @@ -2685,7 +3032,9 @@ "patternProperties": { "[a-zA-Z0-9-_.]+": { "type": "object", - "required": ["jwks_url"], + "required": [ + "jwks_url" + ], "properties": { "ttl": { "type": "string", @@ -2718,7 +3067,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "cookie": { "type": "object", @@ -2749,7 +3102,11 @@ "title": "Session Cookie SameSite Configuration", "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.", "type": "string", - "enum": ["Strict", "Lax", "None"] + "enum": [ + "Strict", + "Lax", + "None" + ] } }, "additionalProperties": false @@ -2759,7 +3116,11 @@ "description": "Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value.", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } }, @@ -2768,7 +3129,9 @@ "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", "type": "string", "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$", - "examples": ["v0.5.0-alpha.1"] + "examples": [ + "v0.5.0-alpha.1" + ] }, "dev": { "type": "boolean" @@ -2792,7 +3155,9 @@ "type": "integer", "minimum": 0, "maximum": 65535, - "examples": [4434], + "examples": [ + 4434 + ], "default": 0 }, "config": { @@ -2864,7 +3229,7 @@ }, "organizations": { "title": "Organizations", - "description": "Secifies which organizations are available. Only effective in the Ory Network.", + "description": "Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network.", "type": "array", "default": [] }, @@ -2898,10 +3263,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] } }, - "required": ["verification"] + "required": [ + "verification" + ] }, { "properties": { @@ -2911,21 +3280,31 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] } }, - "required": ["recovery"] + "required": [ + "recovery" + ] } ] } }, - "required": ["flows"] + "required": [ + "flows" + ] } }, - "required": ["selfservice"] + "required": [ + "selfservice" + ] }, "then": { - "required": ["courier"] + "required": [ + "courier" + ] } }, { @@ -2944,21 +3323,33 @@ ] } }, - "required": ["algorithm"] + "required": [ + "algorithm" + ] } }, - "required": ["ciphers"] + "required": [ + "ciphers" + ] }, "then": { - "required": ["secrets"], + "required": [ + "secrets" + ], "properties": { "secrets": { - "required": ["cipher"] + "required": [ + "cipher" + ] } } } } ], - "required": ["identity", "dsn", "selfservice"], + "required": [ + "identity", + "dsn", + "selfservice" + ], "additionalProperties": false } diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From b29dff3850ba3d8ce04a32d9327222038629925d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:14:13 +0000 Subject: [PATCH 119/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From b192c92d6c969d470d6479bc33dbc351d327c1f9 Mon Sep 17 00:00:00 2001 From: Patrik Date: Thu, 13 Jun 2024 15:25:25 +0200 Subject: [PATCH 120/262] test: deflake session extend config side-effect (#3950) --- .golangci.yml | 6 + cipher/cipher_test.go | 72 ++-- cmd/courier/watch_test.go | 4 +- cmd/hashers/argon2/root.go | 3 + courier/template/load_template_test.go | 2 +- driver/config/config.go | 16 +- driver/config/config_test.go | 100 ++--- driver/config/test_config.go | 75 ++++ driver/factory.go | 2 +- driver/registry_default_test.go | 534 ++++++++++++------------- go.mod | 2 +- hydra/hydra_test.go | 4 +- internal/driver.go | 28 +- internal/testhelpers/config.go | 44 +- internal/testhelpers/network.go | 2 +- persistence/sql/persister_hmac_test.go | 2 +- session/test/persistence.go | 31 +- x/redir_test.go | 12 +- 18 files changed, 525 insertions(+), 414 deletions(-) create mode 100644 driver/config/test_config.go diff --git a/.golangci.yml b/.golangci.yml index 079e952252ba..374c9204ed1b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -25,3 +25,9 @@ run: skip-files: - ".+_test.go" - "corpx/faker.go" + +issues: + exclude: + - "Set is deprecated: use context-based WithConfigValue instead" + - "SetDefaultIdentitySchemaFromRaw is deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead" + - "SetDefaultIdentitySchema is deprecated: Use context-based WithDefaultIdentitySchema instead" diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go index 8cdb0ed0e2ac..eb8ba7e1ba7b 100644 --- a/cipher/cipher_test.go +++ b/cipher/cipher_test.go @@ -9,6 +9,8 @@ import ( "fmt" "testing" + "github.com/ory/x/configx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,10 +20,11 @@ import ( "github.com/ory/kratos/internal" ) +var goodSecret = []string{"secret-thirty-two-character-long"} + func TestCipher(t *testing.T) { ctx := context.Background() - cfg, reg := internal.NewFastRegistryWithMocks(t) - goodSecret := []string{"secret-thirty-two-character-long"} + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySecretsDefault, goodSecret)) ciphers := []cipher.Cipher{ cipher.NewCryptAES(reg), @@ -30,82 +33,71 @@ func TestCipher(t *testing.T) { for _, c := range ciphers { t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) { + t.Parallel() t.Run("case=all_work", func(t *testing.T) { - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - testAllWork(t, c, cfg) + t.Parallel() + + testAllWork(ctx, t, c) }) t.Run("case=encryption_failed", func(t *testing.T) { - // unset secret - err := cfg.Set(ctx, config.ViperKeySecretsCipher, []string{}) - require.NoError(t, err) + t.Parallel() + + ctx := config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) // secret have to be set - _, err = c.Encrypt(context.Background(), []byte("not-empty")) + _, err := c.Encrypt(ctx, []byte("not-empty")) require.Error(t, err) + var hErr *herodot.DefaultError + require.ErrorAs(t, err, &hErr) + assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) - // unset secret - err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) - require.NoError(t, err) + ctx = config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) // bad secret length - _, err = c.Encrypt(context.Background(), []byte("not-empty")) - if e, ok := err.(*herodot.DefaultError); ok { - t.Logf("reason contains: %s", e.Reason()) - } - t.Logf("err type %T contains: %s", err, err.Error()) - require.Error(t, err) + _, err = c.Encrypt(ctx, []byte("not-empty")) + require.ErrorAs(t, err, &hErr) + assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) }) t.Run("case=decryption_failed", func(t *testing.T) { - // set secret - err := cfg.Set(ctx, config.ViperKeySecretsCipher, goodSecret) - require.NoError(t, err) + t.Parallel() - // - _, err = c.Decrypt(context.Background(), hex.EncodeToString([]byte("bad-data"))) + _, err := c.Decrypt(ctx, hex.EncodeToString([]byte("bad-data"))) require.Error(t, err) - _, err = c.Decrypt(context.Background(), "not-empty") + _, err = c.Decrypt(ctx, "not-empty") require.Error(t, err) - // unset secret - err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{}) - require.NoError(t, err) - - _, err = c.Decrypt(context.Background(), "not-empty") + _, err = c.Decrypt(config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") require.Error(t, err) }) }) } + c := cipher.NewNoop(reg) t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) { - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - testAllWork(t, c, cfg) + t.Parallel() + testAllWork(ctx, t, c) }) } -func testAllWork(t *testing.T, c cipher.Cipher, cfg *config.Config) { - ctx := context.Background() - - goodSecret := []string{"secret-thirty-two-character-long"} - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - +func testAllWork(ctx context.Context, t *testing.T, c cipher.Cipher) { message := "my secret message!" - encryptedSecret, err := c.Encrypt(context.Background(), []byte(message)) + encryptedSecret, err := c.Encrypt(ctx, []byte(message)) require.NoError(t, err) - decryptedSecret, err := c.Decrypt(context.Background(), encryptedSecret) + decryptedSecret, err := c.Decrypt(ctx, encryptedSecret) require.NoError(t, err, "encrypted", encryptedSecret) assert.Equal(t, message, string(decryptedSecret)) // data to encrypt return blank result - _, err = c.Encrypt(context.Background(), []byte("")) + _, err = c.Encrypt(ctx, []byte("")) require.NoError(t, err) // empty encrypted data return blank - _, err = c.Decrypt(context.Background(), "") + _, err = c.Decrypt(ctx, "") require.NoError(t, err) } diff --git a/cmd/courier/watch_test.go b/cmd/courier/watch_test.go index 48fd6515f53b..b521e9119a97 100644 --- a/cmd/courier/watch_test.go +++ b/cmd/courier/watch_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ory/kratos/internal" + "github.com/ory/x/configx" ) func TestStartCourier(t *testing.T) { @@ -27,10 +28,9 @@ func TestStartCourier(t *testing.T) { t.Run("case=with metrics", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - _, r := internal.NewFastRegistryWithMocks(t) port, err := freeport.GetFreePort() require.NoError(t, err) - r.Config().Set(ctx, "expose-metrics-port", port) + _, r := internal.NewFastRegistryWithMocks(t, configx.WithValue("expose-metrics-port", port)) go StartCourier(ctx, r) time.Sleep(time.Second) res, err := http.Get("http://" + r.Config().MetricsListenOn(ctx) + "/metrics/prometheus") diff --git a/cmd/hashers/argon2/root.go b/cmd/hashers/argon2/root.go index c5cb76581590..2282f0404d4a 100644 --- a/cmd/hashers/argon2/root.go +++ b/cmd/hashers/argon2/root.go @@ -9,6 +9,8 @@ import ( "reflect" "strings" + "github.com/ory/x/contextx" + "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -70,6 +72,7 @@ func configProvider(cmd *cobra.Command, flagConf *argon2Config) (*argon2Config, cmd.Context(), l, cmd.ErrOrStderr(), + &contextx.Default{}, configx.WithFlags(cmd.Flags()), configx.SkipValidation(), configx.WithContext(cmd.Context()), diff --git a/courier/template/load_template_test.go b/courier/template/load_template_test.go index e6b043aa0c54..1fd245497ca9 100644 --- a/courier/template/load_template_test.go +++ b/courier/template/load_template_test.go @@ -182,7 +182,7 @@ func TestLoadTextTemplate(t *testing.T) { }) t.Run("case=disallowed resources", func(t *testing.T) { - require.NoError(t, reg.Config().GetProvider(ctx).Set(config.ViperKeyClientHTTPNoPrivateIPRanges, true)) + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true)) reg.HTTPClient(ctx).RetryMax = 1 reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond diff --git a/driver/config/config.go b/driver/config/config.go index 05d7ddef52a7..9f3c1b38938b 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -367,13 +367,13 @@ func (s Schemas) FindSchemaByID(id string) (*Schema, error) { return nil, errors.Errorf("unable to find identity schema with id: %s", id) } -func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) *Config { - p, err := New(context.TODO(), l, stdOutOrErr, opts...) +func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) *Config { + p, err := New(context.TODO(), l, stdOutOrErr, ctxer, opts...) require.NoError(t, err) return p } -func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) (*Config, error) { +func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) (*Config, error) { var c *Config opts = append([]configx.OptionModifier{ @@ -402,7 +402,7 @@ func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ... l.UseConfig(p) - c = NewCustom(l, p, stdOutOrErr, &contextx.Default{}) + c = NewCustom(l, p, stdOutOrErr, ctxer) if !p.SkipValidation() { if err := c.validateIdentitySchemas(ctx); err != nil { @@ -518,12 +518,14 @@ func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) { }) } +// Deprecatd: use context-based WithConfigValue instead func (p *Config) Set(ctx context.Context, key string, value interface{}) error { - return p.GetProvider(ctx).Set(key, value) + return p.p.Set(key, value) } +// Deprecated: use context-based WithConfigValue instead func (p *Config) MustSet(ctx context.Context, key string, value interface{}) { - if err := p.GetProvider(ctx).Set(key, value); err != nil { + if err := p.p.Set(key, value); err != nil { p.l.WithError(err).Fatalf("Unable to set \"%s\" to \"%s\".", key, value) } } @@ -859,7 +861,7 @@ func (p *Config) SecretsCipher(ctx context.Context) [][32]byte { result := make([][32]byte, len(cleanSecrets)) for n, s := range secrets { for k, v := range []byte(s) { - result[n][k] = byte(v) + result[n][k] = v } } return result diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 6cb37f100850..dc276eb3a171 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -18,6 +18,8 @@ import ( "testing" "time" + "github.com/ory/x/contextx" + "github.com/ory/x/httpx" "github.com/ory/x/randx" @@ -51,6 +53,7 @@ func TestViperProvider(t *testing.T) { t.Run("suite=loaders", func(t *testing.T) { p := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithContext(ctx), ) @@ -89,6 +92,7 @@ func TestViperProvider(t *testing.T) { pWithFragments := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: "http://test.kratos.ory.sh/#/login", config.ViperKeySelfServiceSettingsURL: "http://test.kratos.ory.sh/#/settings", @@ -105,6 +109,7 @@ func TestViperProvider(t *testing.T) { pWithRelativeFragments := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: "/login", config.ViperKeySelfServiceSettingsURL: "/settings", @@ -130,6 +135,7 @@ func TestViperProvider(t *testing.T) { pWithIncorrectUrls := config.MustNew(t, logger, os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: v, }), @@ -161,6 +167,7 @@ func TestViperProvider(t *testing.T) { t.Run("group=identity", func(t *testing.T) { c := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"), configx.SkipValidation()) @@ -198,7 +205,7 @@ func TestViperProvider(t *testing.T) { }, p.SecretsSession(ctx)) var cipherExpected [32]byte for k, v := range []byte("secret-thirty-two-character-long") { - cipherExpected[k] = byte(v) + cipherExpected[k] = v } assert.Equal(t, [][32]byte{ cipherExpected, @@ -400,7 +407,7 @@ func TestViperProvider(t *testing.T) { func TestBcrypt(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, 4)) require.NoError(t, p.Set(ctx, "dev", false)) @@ -418,7 +425,7 @@ func TestProviderBaseURLs(t *testing.T) { machineHostname = "127.0.0.1" } - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfPublicURL(ctx).String()) assert.Equal(t, "https://"+machineHostname+":4434/", p.SelfAdminURL(ctx).String()) @@ -446,7 +453,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) { machineHostname = "127.0.0.1" } - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfServiceLinkMethodBaseURL(ctx).String()) p.MustSet(ctx, config.ViperKeyLinkBaseURL, "https://example.org/bar") @@ -456,7 +463,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) { func TestViperProvider_Secrets(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) def := p.SecretsDefault(ctx) assert.NotEmpty(t, def) @@ -479,24 +486,25 @@ func TestViperProvider_Defaults(t *testing.T) { }{ { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) }, }, { init: func() *config.Config { return config.MustNew(t, l, os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.defaults.yml"), configx.SkipValidation()) }, }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation()) }, }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.True(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -512,7 +520,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -528,7 +536,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -543,7 +551,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.True(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)) @@ -572,7 +580,7 @@ func TestViperProvider_Defaults(t *testing.T) { } t.Run("suite=ui_url", func(t *testing.T) { - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/login", p.SelfServiceFlowLoginUI(ctx).String()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/settings", p.SelfServiceFlowSettingsUI(ctx).String()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/registration", p.SelfServiceFlowRegistrationUI(ctx).String()) @@ -585,7 +593,7 @@ func TestViperProvider_ReturnTo(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) p.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh/") assert.Equal(t, "https://www.ory.sh/", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.sh/")).String()) @@ -602,7 +610,7 @@ func TestSession(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "ory_kratos_session", p.SessionName(ctx)) p.MustSet(ctx, config.ViperKeySessionName, "ory_session") @@ -629,7 +637,7 @@ func TestCookies(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) t.Run("path", func(t *testing.T) { assert.Equal(t, "/", p.CookiePath(ctx)) @@ -676,14 +684,14 @@ func TestViperProvider_DSN(t *testing.T) { ctx := context.Background() t.Run("case=dsn: memory", func(t *testing.T) { - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) p.MustSet(ctx, config.ViperKeyDSN, "memory") assert.Equal(t, config.DefaultSQLiteMemoryDSN, p.DSN(ctx)) }) t.Run("case=dsn: not memory", func(t *testing.T) { - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) dsn := "sqlite://foo.db?_fk=true" p.MustSet(ctx, config.ViperKeyDSN, dsn) @@ -698,7 +706,7 @@ func TestViperProvider_DSN(t *testing.T) { l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { exitCode = i })) - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, dsn, p.DSN(ctx)) assert.NotEqual(t, 0, exitCode) @@ -714,7 +722,7 @@ func TestViperProvider_ParseURIOrFail(t *testing.T) { l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { exitCode = i })) - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) require.Zero(t, exitCode) const testKey = "testKeyNotUsedInTheRealSchema" @@ -768,7 +776,7 @@ func TestViperProvider_HaveIBeenPwned(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) t.Run("case=hipb: host", func(t *testing.T) { p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedHost, "foo.bar") assert.Equal(t, "foo.bar", p.PasswordPolicyConfig(ctx).HaveIBeenPwnedHost) @@ -806,7 +814,7 @@ func newTestConfig(t *testing.T) (_ *config.Config, _ *test.Hook, exited *bool) exited = new(bool) l.Logger.Hooks.Add(h) l.Logger.ExitFunc = func(code int) { *exited = true } - config := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + config := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) return config, h, exited } @@ -972,7 +980,7 @@ func TestIdentitySchemaValidation(t *testing.T) { l := logrusx.New("kratos-"+tmpConfig.Name(), "test") hook := test.NewLocal(l.Logger) - conf, err := config.New(ctx, l, os.Stderr, configx.WithConfigFiles(tmpConfig.Name())) + conf, err := config.New(ctx, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles(tmpConfig.Name())) assert.NoError(t, err) // clean the hooks since it will throw an event on first boot @@ -986,7 +994,7 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Run("case=skip invalid schema validation", func(t *testing.T) { ctx := ctx - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml"), configx.SkipValidation()) assert.NoError(t, err) @@ -995,7 +1003,7 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Run("case=invalid schema should throw error", func(t *testing.T) { ctx := ctx var stdErr bytes.Buffer - _, err := config.New(ctx, logrusx.New("", ""), &stdErr, + _, err := config.New(ctx, logrusx.New("", ""), &stdErr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml")) assert.Error(t, err) assert.Contains(t, err.Error(), "minimum 1 properties allowed, but found 0") @@ -1013,7 +1021,7 @@ func TestIdentitySchemaValidation(t *testing.T) { err := make(chan error, 1) go func(err chan error) { - _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml")) err <- e }(err) @@ -1068,7 +1076,7 @@ func TestPasswordless(t *testing.T) { t.Parallel() ctx := context.Background() - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation(), configx.WithValue(config.ViperKeyWebAuthnPasswordless, true)) require.NoError(t, err) @@ -1083,7 +1091,7 @@ func TestPasswordlessCode(t *testing.T) { ctx := context.Background() - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation(), configx.WithValue(config.ViperKeySelfServiceStrategyConfig+".code", map[string]interface{}{ "passwordless_enabled": true, @@ -1100,7 +1108,7 @@ func TestChangeMinPasswordLength(t *testing.T) { t.Run("case=must fail on minimum password length below enforced minimum", func(t *testing.T) { ctx := context.Background() - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithValue(config.ViperKeyPasswordMinLength, 5)) @@ -1110,7 +1118,7 @@ func TestChangeMinPasswordLength(t *testing.T) { t.Run("case=must not fail on minimum password length above enforced minimum", func(t *testing.T) { ctx := context.Background() - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithValue(config.ViperKeyPasswordMinLength, 9)) @@ -1123,14 +1131,14 @@ func TestCourierEmailHTTP(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.email.http.yaml"), configx.SkipValidation()) assert.Equal(t, "http", conf.CourierEmailStrategy(ctx)) snapshotx.SnapshotT(t, conf.CourierEmailRequestConfig(ctx)) }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "smtp", conf.CourierEmailStrategy(ctx)) }) @@ -1140,7 +1148,7 @@ func TestCourierChannels(t *testing.T) { t.Parallel() ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) channelConfig, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1152,7 +1160,7 @@ func TestCourierChannels(t *testing.T) { }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) channelConfig, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1171,7 +1179,7 @@ func TestCourierChannels(t *testing.T) { "smtp://username:pass%2Fword@email-smtp.eu-west-3.amazonaws.com:587/", } { t.Run("case="+tc, func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) require.NoError(t, err) cs, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1187,13 +1195,13 @@ func TestCourierMessageTTL(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.message_retries.yaml"), configx.SkipValidation()) assert.Equal(t, conf.CourierMessageRetries(ctx), 10) }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, conf.CourierMessageRetries(ctx), 5) }) } @@ -1203,7 +1211,7 @@ func TestOAuth2Provider(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.oauth2_provider.yaml"), configx.SkipValidation()) assert.Equal(t, "https://oauth2_provider/", conf.OAuth2ProviderURL(ctx).String()) assert.Equal(t, http.Header{"Authorization": {"Basic"}}, conf.OAuth2ProviderHeader(ctx)) @@ -1211,7 +1219,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Empty(t, conf.OAuth2ProviderURL(ctx)) assert.Empty(t, conf.OAuth2ProviderHeader(ctx)) assert.False(t, conf.OAuth2ProviderOverrideReturnTo(ctx)) @@ -1223,7 +1231,7 @@ func TestWebauthn(t *testing.T) { ctx := context.Background() t.Run("case=multiple origins", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.origins.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1236,7 +1244,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=one origin", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.origin.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1247,7 +1255,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=id as origin", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1258,7 +1266,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=invalid", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.invalid.yaml")) assert.Error(t, err) }) @@ -1269,19 +1277,19 @@ func TestCourierTemplatesConfig(t *testing.T) { ctx := context.Background() t.Run("case=partial template update allowed", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.partial.templates.yaml")) assert.NoError(t, err) }) t.Run("case=load remote template with fallback template overrides path", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) assert.NoError(t, err) }) t.Run("case=courier template helper", func(t *testing.T) { - c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) assert.NoError(t, err) @@ -1323,7 +1331,7 @@ func TestCleanup(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml")) t.Run("group=cleanup config", func(t *testing.T) { diff --git a/driver/config/test_config.go b/driver/config/test_config.go new file mode 100644 index 000000000000..459ae15ac89c --- /dev/null +++ b/driver/config/test_config.go @@ -0,0 +1,75 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "strings" + + "github.com/knadh/koanf/maps" + + "github.com/ory/kratos/embedx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" +) + +type ( + TestConfigProvider struct { + contextx.Contextualizer + Options []configx.OptionModifier + } + contextKey int + mapProvider map[string]any +) + +func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { + return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...) +} + +func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + config = t.Contextualizer.Config(ctx, config) + values, ok := ctx.Value(contextConfigKey).(mapProvider) + if !ok { + return config + } + config, err := t.NewProvider(ctx, configx.WithValues(values)) + if err != nil { + // This is not production code. The provider is only used in tests. + panic(err) + } + return config +} + +const contextConfigKey contextKey = 1 + +var ( + _ contextx.Contextualizer = (*TestConfigProvider)(nil) +) + +func WithConfigValue(ctx context.Context, key string, value any) context.Context { + return WithConfigValues(ctx, map[string]any{key: value}) +} + +func WithConfigValues(ctx context.Context, newValues map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).(mapProvider) + if !ok { + values = make(mapProvider) + } + expandedValues := make([]map[string]any, 0, len(newValues)) + for k, v := range newValues { + parts := strings.Split(k, ".") + val := map[string]any{parts[len(parts)-1]: v} + if len(parts) > 1 { + for i := len(parts) - 2; i >= 0; i-- { + val = map[string]any{parts[i]: val} + } + } + expandedValues = append(expandedValues, val) + } + for _, v := range expandedValues { + maps.Merge(v, values) + } + + return context.WithValue(ctx, contextConfigKey, values) +} diff --git a/driver/factory.go b/driver/factory.go index e3470d3cffd9..da0dd5601e2b 100644 --- a/driver/factory.go +++ b/driver/factory.go @@ -38,7 +38,7 @@ func NewWithoutInit(ctx context.Context, stdOutOrErr io.Writer, sl *servicelocat c := newOptions(dOpts).config if c == nil { var err error - c, err = config.New(ctx, l, stdOutOrErr, opts...) + c, err = config.New(ctx, l, stdOutOrErr, sl.Contextualizer(), opts...) if err != nil { l.WithError(err).Error("Unable to instantiate configuration.") return nil, err diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 009dd76173d8..020517a41159 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -10,6 +10,8 @@ import ( "os" "testing" + "github.com/ory/x/contextx" + "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/verification" @@ -34,26 +36,27 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Parallel() ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("type=verification", func(t *testing.T) { t.Parallel() // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []verification.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return []verification.PreHookExecutor{ @@ -64,8 +67,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreVerificationHooks(ctx) @@ -79,6 +83,7 @@ func TestDriverDefault_Hooks(t *testing.T) { for _, tc := range []struct { uc string prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []verification.PostHookExecutor }{ { @@ -88,11 +93,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Multiple web_hooks configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor { return []verification.PostHookExecutor{ @@ -103,8 +108,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostVerificationHooks(ctx) @@ -120,21 +126,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRecoveryBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return []recovery.PreHookExecutor{ @@ -145,8 +150,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreRecoveryHooks(ctx) @@ -159,21 +165,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PostHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return nil }, }, { uc: "Multiple web_hooks configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRecoveryAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return []recovery.PostHookExecutor{ @@ -184,8 +189,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostRecoveryHooks(ctx) @@ -201,12 +207,11 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []registration.PreHookExecutor }{ { - uc: "No hooks configured", - prep: func(conf *config.Config) {}, + uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{ hook.NewTwoStepRegistration(reg), @@ -215,11 +220,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRegistrationBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{ @@ -231,8 +236,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreRegistrationHooks(ctx) @@ -245,21 +251,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return nil }, }, { uc: "Only session hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ {"hook": "session"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -270,12 +275,12 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A session hook and a web_hook are configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "session"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -287,11 +292,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -302,15 +307,15 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "session"}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + }, + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -322,10 +327,10 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "show_verification_ui is configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ {"hook": "show_verification_ui"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -335,8 +340,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) @@ -352,21 +358,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []login.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceLoginBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return []login.PreHookExecutor{ @@ -377,8 +382,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreLoginHooks(ctx) @@ -391,20 +397,19 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []login.PostHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return nil }, }, { uc: "Only revoke_active_sessions hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "revoke_active_sessions"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -414,10 +419,10 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Only require_verified_address hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "require_verified_address"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -427,12 +432,12 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A revoke_active_sessions hook, require_verified_address hook and a web_hook are configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "require_verified_address"}, {"hook": "revoke_active_sessions"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -444,11 +449,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -459,15 +464,15 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "revoke_active_sessions"}, {"hook": "require_verified_address"}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + }, + config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -479,8 +484,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword) @@ -496,21 +502,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []settings.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceSettingsBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return []settings.PreHookExecutor{ @@ -521,8 +526,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreSettingsHooks(ctx) @@ -535,18 +541,17 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return nil }, }, { uc: "Only verify hook configured for the strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, // I think this is a bug as there is a hook named verify defined for both profile and password // strategies. Instead of using it, the code makes use of the property used above and which // is defined in an entirely different flow (verification). @@ -559,11 +564,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A verify hook and a web_hook are configured for profile strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + config: map[string]any{ + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}}, + }, + config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ @@ -574,11 +579,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ @@ -589,14 +594,14 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ @@ -607,8 +612,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostSettingsPostPersistHooks(ctx, "profile") @@ -623,62 +629,64 @@ func TestDriverDefault_Hooks(t *testing.T) { func TestDriverDefault_Strategies(t *testing.T) { t.Parallel() ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("case=registration", func(t *testing.T) { t.Parallel() for _, tc := range []struct { name string - prep func(conf *config.Config) + config map[string]any expect []string }{ { name: "no strategies", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"profile"}, }, { name: "only password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "profile"}, }, { name: "oidc and password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "profile"}, }, { name: "oidc, password and totp", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "profile"}, }, { name: "password and code", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, expect: []string{"password", "profile", "code"}, }, } { t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() - s := reg.RegistrationStrategies(context.Background()) + ctx := config.WithConfigValues(ctx, tc.config) + s := reg.RegistrationStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) @@ -689,68 +697,69 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run("case=login", func(t *testing.T) { t.Parallel() + for _, tc := range []struct { name string - prep func(conf *config.Config) + config map[string]any expect []string }{ { name: "no strategies", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, }, { name: "only password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password"}, }, { name: "oidc and password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc"}, }, { name: "oidc, password and totp", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "totp"}, }, { name: "password and code", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, expect: []string{"password", "code"}, }, { name: "code is enabled if passwordless_enabled is true", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.passwordless_enabled": true, }, expect: []string{"code"}, }, } { t.Run(fmt.Sprintf("run=%s", tc.name), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() - s := reg.LoginStrategies(context.Background()) + ctx := config.WithConfigValues(ctx, tc.config) + s := reg.LoginStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) @@ -762,27 +771,28 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run("case=recovery", func(t *testing.T) { t.Parallel() for k, tc := range []struct { - prep func(conf *config.Config) + config map[string]any expect []string }{ { - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".link.enabled": false, }, }, { - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".link.enabled": true, }, expect: []string{"code", "link"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) - s := reg.RecoveryStrategies(context.Background()) + s := reg.RecoveryStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].RecoveryStrategyID()) @@ -796,81 +806,55 @@ func TestDriverDefault_Strategies(t *testing.T) { l := logrusx.New("", "") for k, tc := range []struct { - prep func(t *testing.T) *config.Config - expect []string + configOptions []configx.OptionModifier + expect []string }{ { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, - }), - configx.SkipValidation()) - return c - }, - }, - { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - }), - configx.SkipValidation()) - return c - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, + })}, + }, + { + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + })}, expect: []string{"profile"}, }, { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, - }), - configx.SkipValidation()) - return c - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + })}, expect: []string{"profile", "totp"}, }, { - prep: func(t *testing.T) *config.Config { - return config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - }), - configx.SkipValidation()) - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + })}, expect: []string{"password", "profile"}, }, { - prep: func(t *testing.T) *config.Config { - return config.MustNew(t, l, - os.Stderr, - configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), - configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), - configx.SkipValidation()) + configOptions: []configx.OptionModifier{ + configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), + configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), }, expect: []string{"password", "profile"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { - conf := tc.prep(t) + conf := config.MustNew(t, l, os.Stderr, &contextx.Default{}, append(tc.configOptions, configx.SkipValidation())...) - reg, err := driver.NewRegistryFromDSN(ctx, conf, logrusx.New("", "")) + reg, err := driver.NewRegistryFromDSN(ctx, conf, l) require.NoError(t, err) - s := reg.SettingsStrategies(context.Background()) + s := reg.SettingsStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { @@ -924,12 +908,16 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { func TestGetActiveRecoveryStrategy(t *testing.T) { t.Parallel() - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("returns error if active strategy is disabled", func(t *testing.T) { - conf.Set(context.Background(), "selfservice.methods.code.enabled", false) - conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code") + ctx := config.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceRecoveryUse: "code", + }) - _, err := reg.GetActiveRecoveryStrategy(context.Background()) + _, err := reg.GetActiveRecoveryStrategy(ctx) require.Error(t, err) }) @@ -938,10 +926,12 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true) - conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, sID) + ctx := config.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceRecoveryUse: sID, + }) - s, err := reg.GetActiveRecoveryStrategy(context.Background()) + s, err := reg.GetActiveRecoveryStrategy(ctx) require.NoError(t, err) require.Equal(t, sID, s.RecoveryStrategyID()) }) @@ -951,12 +941,14 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { func TestGetActiveVerificationStrategy(t *testing.T) { t.Parallel() - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { - conf.Set(context.Background(), "selfservice.methods.code.enabled", false) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, "code") - - _, err := reg.GetActiveVerificationStrategy(context.Background()) + ctx := config.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceVerificationUse: "code", + }) + _, err := reg.GetActiveVerificationStrategy(ctx) require.Error(t, err) }) @@ -965,10 +957,12 @@ func TestGetActiveVerificationStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, sID) + ctx := config.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceVerificationUse: sID, + }) - s, err := reg.GetActiveVerificationStrategy(context.Background()) + s, err := reg.GetActiveVerificationStrategy(ctx) require.NoError(t, err) require.Equal(t, sID, s.VerificationStrategyID()) }) diff --git a/go.mod b/go.mod index 537942ebacbd..67e7a524c134 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ory/kratos -go 1.21 +go 1.22 replace ( github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb diff --git a/hydra/hydra_test.go b/hydra/hydra_test.go index b2e252be5b18..d022ae6021cf 100644 --- a/hydra/hydra_test.go +++ b/hydra/hydra_test.go @@ -13,6 +13,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" "github.com/ory/x/configx" + "github.com/ory/x/contextx" "github.com/ory/x/logrusx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" @@ -25,11 +26,12 @@ func requestFromChallenge(s string) *http.Request { func TestGetLoginChallengeID(t *testing.T) { uuidChallenge := "b346a452-e8fb-4828-8ef8-a4dbc98dc23a" blobChallenge := "1337deadbeefcafe" - defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) configWithHydra := config.MustNew( t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{ config.ViperKeyOAuth2ProviderURL: "https://hydra", diff --git a/internal/driver.go b/internal/driver.go index a6f1f13d7954..3499a83b5b9b 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -9,9 +9,10 @@ import ( "runtime" "testing" + "github.com/ory/x/contextx" + "github.com/sirupsen/logrus" - "github.com/ory/x/contextx" "github.com/ory/x/jsonnetsecure" "github.com/gofrs/uuid" @@ -36,9 +37,8 @@ func init() { }) } -func NewConfigurationWithDefaults(t testing.TB) *config.Config { - c := config.MustNew(t, logrusx.New("", ""), - os.Stderr, +func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) *config.Config { + configOpts := append([]configx.OptionModifier{ configx.WithValues(map[string]interface{}{ "log.level": "error", config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), @@ -53,14 +53,19 @@ func NewConfigurationWithDefaults(t testing.TB) *config.Config { config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, }), configx.SkipValidation(), + }, opts...) + c := config.MustNew(t, logrusx.New("", ""), + os.Stderr, + &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts}, + configOpts..., ) return c } // NewFastRegistryWithMocks returns a registry with several mocks and an SQLite in memory database that make testing // easier and way faster. This suite does not work for e2e or advanced integration tests. -func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDefault) { - conf, reg := NewRegistryDefaultWithDSN(t, "") +func NewFastRegistryWithMocks(t *testing.T, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { + conf, reg := NewRegistryDefaultWithDSN(t, "", opts...) reg.WithCSRFTokenGenerator(x.FakeCSRFTokenGenerator) reg.WithCSRFHandler(x.NewFakeCSRFHandler("")) reg.WithHooks(map[string]func(config.SelfServiceHook) interface{}{ @@ -76,16 +81,17 @@ func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDef } // NewRegistryDefaultWithDSN returns a more standard registry without mocks. Good for e2e and advanced integration testing! -func NewRegistryDefaultWithDSN(t testing.TB, dsn string) (*config.Config, *driver.RegistryDefault) { +func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { ctx := context.Background() - c := NewConfigurationWithDefaults(t) - c.MustSet(ctx, config.ViperKeyDSN, stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t))) + c := NewConfigurationWithDefaults(t, append(opts, configx.WithValues(map[string]interface{}{ + config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)), + "dev": true, + }))...) reg, err := driver.NewRegistryFromDSN(ctx, c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel))) require.NoError(t, err) - reg.Config().MustSet(ctx, "dev", true) pool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) t.Cleanup(pool.Close) - require.NoError(t, reg.Init(context.Background(), &contextx.Default{}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) + require.NoError(t, reg.Init(context.Background(), &config.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) require.NoError(t, reg.Persister().MigrateUp(context.Background())) // always migrate up actual, err := reg.Persister().DetermineNetwork(context.Background()) diff --git a/internal/testhelpers/config.go b/internal/testhelpers/config.go index 2a24709c0745..8e17a6ab3a12 100644 --- a/internal/testhelpers/config.go +++ b/internal/testhelpers/config.go @@ -8,11 +8,10 @@ import ( "encoding/base64" "testing" - "github.com/ory/kratos/driver/config" - "github.com/spf13/pflag" "github.com/stretchr/testify/require" + "github.com/ory/kratos/driver/config" "github.com/ory/x/configx" "github.com/ory/x/randx" ) @@ -24,6 +23,20 @@ func UseConfigFile(t *testing.T, path string) *pflag.FlagSet { return flags } +func DefaultIdentitySchemaConfig(url string) map[string]any { + return map[string]any{ + config.ViperKeyDefaultIdentitySchemaID: "default", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: url}, + }, + } +} + +func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context { + return config.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url)) +} + +// Deprecated: Use context-based WithDefaultIdentitySchema instead func SetDefaultIdentitySchema(conf *config.Config, url string) func() { schemaUrl, _ := conf.DefaultIdentityTraitsSchemaURL(context.Background()) conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default") @@ -37,13 +50,29 @@ func SetDefaultIdentitySchema(conf *config.Config, url string) func() { } } -// UseIdentitySchema registeres an identity schema in the config with a random ID and returns the ID +// WithAddIdentitySchema registers an identity schema in the config with a random ID and returns the ID +// +// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes +func WithAddIdentitySchema(ctx context.Context, t *testing.T, conf *config.Config, url string) (context.Context, string) { + id := randx.MustString(16, randx.Alpha) + schemas, err := conf.IdentityTraitsSchemas(ctx) + require.NoError(t, err) + + return config.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ + ID: id, + URL: url, + })), id +} + +// UseIdentitySchema registers an identity schema in the config with a random ID and returns the ID // -// It also registeres a test cleanup function, to reset the schemas to the original values, after the test finishes +// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes +// Deprecated: Use context-based WithAddIdentitySchema instead func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string) { id = randx.MustString(16, randx.Alpha) schemas, err := conf.IdentityTraitsSchemas(context.Background()) require.NoError(t, err) + conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ ID: id, URL: url, @@ -54,7 +83,12 @@ func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string return id } -// SetDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string. +// WithDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string. +func WithDefaultIdentitySchemaFromRaw(ctx context.Context, schema []byte) context.Context { + return WithDefaultIdentitySchema(ctx, "base64://"+base64.URLEncoding.EncodeToString(schema)) +} + +// Deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead func SetDefaultIdentitySchemaFromRaw(conf *config.Config, schema []byte) { conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default") conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{ diff --git a/internal/testhelpers/network.go b/internal/testhelpers/network.go index 888f46b583f5..10978dba6b05 100644 --- a/internal/testhelpers/network.go +++ b/internal/testhelpers/network.go @@ -20,7 +20,7 @@ func NewNetworkUnlessExisting(t *testing.T, ctx context.Context, p persistence.P } n := networkx.NewNetwork() - require.NoError(t, p.GetConnection(context.Background()).Create(n)) + require.NoError(t, p.GetConnection(ctx).Create(n)) return n.ID, p.WithNetworkID(n.ID) } diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index c7adcdce3a1e..7b8cc8575368 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -64,7 +64,7 @@ var _ persisterDependencies = &logRegistryOnly{} func TestPersisterHMAC(t *testing.T) { ctx := context.Background() - conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"foobarbaz"}) c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"}) require.NoError(t, err) diff --git a/session/test/persistence.go b/session/test/persistence.go index 8e8cbfeb18b2..0db6964468d8 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -42,7 +42,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + ctx := testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/identity.schema.json") t.Run("case=not found", func(t *testing.T) { _, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing) @@ -611,10 +611,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) - t.Cleanup(func() { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) - }) + ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) var expected session.Session require.NoError(t, faker.FakeData(&expected)) @@ -629,23 +626,19 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour) - t.Cleanup(func() { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) - }) + ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) var expected session.Session require.NoError(t, faker.FakeData(&expected)) expected.ExpiresAt = time.Now().Add(time.Hour).UTC() require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) require.NoError(t, p.UpsertSession(ctx, &expected)) - expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute) + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt require.NoError(t, p.ExtendSession(ctx, expected.ID)) actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) require.NoError(t, err) - assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute)) + assert.GreaterOrEqual(t, 10*time.Second, expectedExpiry.Sub(actual.ExpiresAt).Abs()) }) t.Run("extend session lifespan on CockroachDB", func(t *testing.T) { @@ -653,23 +646,19 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { t.Skip("Skipping test because driver is not CockroachDB") } - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour) - t.Cleanup(func() { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) - }) + ctx := config.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour) - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) var expected session.Session require.NoError(t, faker.FakeData(&expected)) expected.ExpiresAt = time.Now().Add(time.Hour).UTC() require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) require.NoError(t, p.UpsertSession(ctx, &expected)) - expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute) + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt - var foundExpectedCockroachError bool + foundExpectedCockroachError := false g := errgroup.Group{} - for i := 0; i < 10; i++ { + for range 10 { g.Go(func() error { err := p.ExtendSession(ctx, expected.ID) if errors.Is(err, sqlcon.ErrNoRows) { @@ -683,7 +672,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) require.NoError(t, err) - assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute)) + assert.LessOrEqual(t, expectedExpiry.Sub(actual.ExpiresAt).Abs(), 10*time.Second) assert.True(t, foundExpectedCockroachError, "We expect to find a not found error caused by ... FOR UPDATE SKIP LOCKED") }) } diff --git a/x/redir_test.go b/x/redir_test.go index bbb8417f8b91..1c4a191b429b 100644 --- a/x/redir_test.go +++ b/x/redir_test.go @@ -4,7 +4,6 @@ package x_test import ( - "context" "fmt" "io" "net/http" @@ -12,6 +11,8 @@ import ( "strings" "testing" + "github.com/ory/x/configx" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,17 +23,16 @@ import ( ) func TestRedirectToPublicAdminRoute(t *testing.T) { - ctx := context.Background() - conf, reg := internal.NewFastRegistryWithMocks(t) pub := x.NewRouterPublic() adm := x.NewRouterAdmin() adminTS := httptest.NewServer(adm) pubTS := httptest.NewServer(pub) t.Cleanup(pubTS.Close) t.Cleanup(adminTS.Close) - - conf.MustSet(ctx, config.ViperKeyAdminBaseURL, adminTS.URL) - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, pubTS.URL) + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyAdminBaseURL: adminTS.URL, + config.ViperKeyPublicBaseURL: pubTS.URL, + })) pub.POST("/privileged", x.RedirectToAdminRoute(reg)) pub.POST("/admin/privileged", x.RedirectToAdminRoute(reg)) From 61f87d90bd67e5bb1f00ee110d986e4f72fc4c91 Mon Sep 17 00:00:00 2001 From: Patrik Date: Mon, 17 Jun 2024 12:24:31 +0200 Subject: [PATCH 121/262] test: deflake and parallelize persister tests (#3953) --- driver/config/test_config.go | 39 ++--- identity/manager_test.go | 133 +++++++++--------- identity/test/pool.go | 42 +++--- internal/client-go/go.sum | 1 + internal/driver.go | 2 +- persistence/sql/persister_cleanup_test.go | 18 +++ persistence/sql/persister_code.go | 2 +- persistence/sql/persister_errorx.go | 36 ++--- persistence/sql/persister_hmac.go | 18 +-- persistence/sql/persister_hmac_test.go | 50 ++++--- persistence/sql/persister_recovery.go | 2 +- persistence/sql/persister_test.go | 77 +++++----- persistence/sql/persister_verification.go | 2 +- selfservice/flow/recovery/test/persistence.go | 4 +- selfservice/flow/settings/test/persistence.go | 5 +- .../flow/verification/test/persistence.go | 5 +- .../sessiontokenexchange/test/persistence.go | 4 +- selfservice/strategy/code/test/persistence.go | 5 +- selfservice/strategy/link/test/persistence.go | 5 +- session/test/persistence.go | 2 - 20 files changed, 226 insertions(+), 226 deletions(-) diff --git a/driver/config/test_config.go b/driver/config/test_config.go index 459ae15ac89c..c95fba7b7876 100644 --- a/driver/config/test_config.go +++ b/driver/config/test_config.go @@ -5,9 +5,6 @@ package config import ( "context" - "strings" - - "github.com/knadh/koanf/maps" "github.com/ory/kratos/embedx" "github.com/ory/x/configx" @@ -19,8 +16,7 @@ type ( contextx.Contextualizer Options []configx.OptionModifier } - contextKey int - mapProvider map[string]any + contextKey int ) func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { @@ -29,11 +25,15 @@ func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.Op func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { config = t.Contextualizer.Config(ctx, config) - values, ok := ctx.Value(contextConfigKey).(mapProvider) + values, ok := ctx.Value(contextConfigKey).([]map[string]any) if !ok { return config } - config, err := t.NewProvider(ctx, configx.WithValues(values)) + opts := make([]configx.OptionModifier, 0, len(values)) + for _, v := range values { + opts = append(opts, configx.WithValues(v)) + } + config, err := t.NewProvider(ctx, opts...) if err != nil { // This is not production code. The provider is only used in tests. panic(err) @@ -51,25 +51,14 @@ func WithConfigValue(ctx context.Context, key string, value any) context.Context return WithConfigValues(ctx, map[string]any{key: value}) } -func WithConfigValues(ctx context.Context, newValues map[string]any) context.Context { - values, ok := ctx.Value(contextConfigKey).(mapProvider) +func WithConfigValues(ctx context.Context, setValues map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) if !ok { - values = make(mapProvider) - } - expandedValues := make([]map[string]any, 0, len(newValues)) - for k, v := range newValues { - parts := strings.Split(k, ".") - val := map[string]any{parts[len(parts)-1]: v} - if len(parts) > 1 { - for i := len(parts) - 2; i >= 0; i-- { - val = map[string]any{parts[i]: val} - } - } - expandedValues = append(expandedValues, val) - } - for _, v := range expandedValues { - maps.Merge(v, values) + values = make([]map[string]any, 0) } + newValues := make([]map[string]any, len(values), len(values)+1) + copy(newValues, values) + newValues = append(newValues, setValues) - return context.WithValue(ctx, contextConfigKey, values) + return context.WithValue(ctx, contextConfigKey, newValues) } diff --git a/identity/manager_test.go b/identity/manager_test.go index e0346b8ee0c0..f45a3f05e4fc 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -4,11 +4,11 @@ package identity_test import ( - "context" "fmt" "testing" "time" + "github.com/ory/x/configx" "github.com/ory/x/pointerx" "github.com/ory/x/sqlcon" @@ -29,17 +29,17 @@ import ( ) func TestManager(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/manager.schema.json") - extensionSchemaID := testhelpers.UseIdentitySchema(t, conf, "file://./stub/extension.schema.json") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") - conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{ + config.ViperKeyPublicBaseURL: "https://www.ory.sh/", + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeySelfServiceRegistrationLoginHints: true, + }), configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/manager.schema.json"))) + ctx, extensionSchemaID := testhelpers.WithAddIdentitySchema(ctx, t, conf, "file://./stub/extension.schema.json") t.Run("case=should fail to create because validation fails", func(t *testing.T) { i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) i.Traits = identity.Traits(`{"email":"not an email"}`) - require.Error(t, reg.IdentityManager().Create(context.Background(), i)) + require.Error(t, reg.IdentityManager().Create(ctx, i)) }) newTraits := func(email string, unprotected string) identity.Traits { @@ -62,7 +62,7 @@ func TestManager(t *testing.T) { } checkExtensionFieldsForIdentities := func(t *testing.T, expected string, original *identity.Identity) { - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) identities := []identity.Identity{*original, *fromStore} for k := range identities { @@ -75,7 +75,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) checkExtensionFieldsForIdentities(t, email, original) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) @@ -87,7 +87,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) @@ -104,7 +104,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.AuthenticatorAssuranceLevel1, got) @@ -126,7 +126,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.AuthenticatorAssuranceLevel2, got) @@ -143,7 +143,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) @@ -153,7 +153,7 @@ func TestManager(t *testing.T) { t.Run("case=should expose validation errors with option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"not an email"}`) - err := reg.IdentityManager().Create(context.Background(), original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion) + err := reg.IdentityManager().Create(ctx, original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion) require.Error(t, err) assert.Contains(t, err.Error(), "\"not an email\" is not valid \"email\"") }) @@ -161,7 +161,7 @@ func TestManager(t *testing.T) { t.Run("case=should not expose validation errors without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"not an email"}`) - err := reg.IdentityManager().Create(context.Background(), original) + err := reg.IdentityManager().Create(ctx, original) require.Error(t, err) assert.NotContains(t, err.Error(), "\"not an email\" is not valid \"email\"") }) @@ -186,10 +186,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_creds", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_creds", creds) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -210,10 +210,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_webauthn", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_webauthn", nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -235,10 +235,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_creds", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_creds", creds) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -270,10 +270,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_creds", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_creds", creds) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -300,10 +300,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, field, creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, field, nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -329,10 +329,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, field, creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, field, nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -357,10 +357,10 @@ func TestManager(t *testing.T) { t.Run("case=should update identity and update extension fields", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("baz@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Traits = newTraits("bar@ory.sh", "") - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) checkExtensionFieldsForIdentities(t, "bar@ory.sh", original) }) @@ -369,7 +369,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Credentials = map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: { Type: identity.CredentialsTypePassword, @@ -377,7 +377,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String) }) @@ -392,16 +392,16 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String) - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String, "Updating without changes should not change AAL") original.Credentials[identity.CredentialsTypeTOTP] = identity.Credentials{ Type: identity.CredentialsTypeTOTP, Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, original.AvailableAAL.String) }) @@ -409,7 +409,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Credentials = map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypeTOTP: { Type: identity.CredentialsTypeTOTP, @@ -417,7 +417,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.True(t, original.AvailableAAL.Valid) assert.EqualValues(t, identity.NoAuthenticatorAssuranceLevel, original.AvailableAAL.String) }) @@ -425,14 +425,14 @@ func TestManager(t *testing.T) { t.Run("case=should not update protected traits without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-update-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Traits = newTraits("email-update-2@ory.sh", "") - err := reg.IdentityManager().Update(context.Background(), original) + err := reg.IdentityManager().Update(ctx, original) require.Error(t, err) assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -482,21 +482,21 @@ func TestManager(t *testing.T) { originalEmail := x.NewUUID().String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(originalEmail, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) checkExtensionFields(fromStore, originalEmail)(t) newEmail := x.NewUUID().String() + "@ory.sh" original.Traits = newTraits(newEmail, "") - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) - fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) checkExtensionFields(fromStore, newEmail)(t) - recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(context.Background(), 0, 500) + recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(ctx, 0, 500) require.NoError(t, err) var foundRecoveryAddress bool @@ -508,7 +508,7 @@ func TestManager(t *testing.T) { } require.True(t, foundRecoveryAddress) - verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(context.Background(), 0, 500) + verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(ctx, 0, 500) require.NoError(t, err) var foundVerifiableAddress bool for _, a := range verifiableAddresses { @@ -569,13 +569,13 @@ func TestManager(t *testing.T) { t.Run("case=should update protected traits with option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-updatetraits-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) require.NoError(t, reg.IdentityManager().UpdateTraits( - context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", ""), + ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", ""), identity.ManagerAllowWriteProtectedTraits)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -585,17 +585,17 @@ func TestManager(t *testing.T) { t.Run("case=should update identity and update extension fields", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`) - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) // These should all fail because they modify existing keys - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`))) - require.NoError(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`))) + require.NoError(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`))) checkExtensionFieldsForIdentities(t, "baz@ory.sh", original) - actual, err := reg.IdentityPool().GetIdentity(context.Background(), original.ID, identity.ExpandNothing) + actual, err := reg.IdentityPool().GetIdentity(ctx, original.ID, identity.ExpandNothing) require.NoError(t, err) assert.JSONEq(t, `{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`, string(actual.Traits)) }) @@ -603,14 +603,14 @@ func TestManager(t *testing.T) { t.Run("case=should not update protected traits without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-updatetraits-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) err := reg.IdentityManager().UpdateTraits( - context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", "")) + ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", "")) require.Error(t, err) assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -619,7 +619,7 @@ func TestManager(t *testing.T) { }) t.Run("method=ConflictingIdentity", func(t *testing.T) { - ctx := context.Background() + ctx := ctx conflicOnIdentifier := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) conflicOnIdentifier.Traits = identity.Traits(`{"email":"conflict-on-identifier@example.com"}`) @@ -682,12 +682,13 @@ func TestManager(t *testing.T) { } func TestManagerNoDefaultNamedSchema(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "user_v0") - conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ - {ID: "user_v0", URL: "file://./stub/manager.schema.json"}, - }) - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{ + config.ViperKeyDefaultIdentitySchemaID: "user_v0", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "user_v0", URL: "file://./stub/manager.schema.json"}, + }, + config.ViperKeyPublicBaseURL: "https://www.ory.sh/", + })) t.Run("case=should create identity with default schema", func(t *testing.T) { stateChangedAt := sqlxx.NullTime(time.Now().UTC()) @@ -697,6 +698,6 @@ func TestManagerNoDefaultNamedSchema(t *testing.T) { State: identity.StateActive, StateChangedAt: &stateChangedAt, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) }) } diff --git a/identity/test/pool.go b/identity/test/pool.go index 450b5c1ea881..bf6d114b8510 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -36,12 +36,11 @@ import ( "github.com/ory/x/urlx" ) -func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) { +func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) { return func(t *testing.T) { - exampleServerURL := urlx.ParseOrPanic("http://example.com") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, exampleServerURL.String()) - nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + exampleServerURL := urlx.ParseOrPanic("http://example.com") expandSchema := schema.Schema{ ID: "expandSchema", URL: urlx.ParseOrPanic("file://./stub/expand.schema.json"), @@ -62,22 +61,25 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, URL: urlx.ParseOrPanic("file://./stub/handler/multiple_emails.schema.json"), RawURL: "file://./stub/identity-2.schema.json", } - conf.MustSet(ctx, config.ViperKeyIdentitySchemas, []config.Schema{ - { - ID: altSchema.ID, - URL: altSchema.RawURL, - }, - { - ID: defaultSchema.ID, - URL: defaultSchema.RawURL, - }, - { - ID: expandSchema.ID, - URL: expandSchema.RawURL, - }, - { - ID: multipleEmailsSchema.ID, - URL: multipleEmailsSchema.RawURL, + ctx := config.WithConfigValues(ctx, map[string]any{ + config.ViperKeyPublicBaseURL: exampleServerURL.String(), + config.ViperKeyIdentitySchemas: []config.Schema{ + { + ID: altSchema.ID, + URL: altSchema.RawURL, + }, + { + ID: defaultSchema.ID, + URL: defaultSchema.RawURL, + }, + { + ID: expandSchema.ID, + URL: expandSchema.RawURL, + }, + { + ID: multipleEmailsSchema.ID, + URL: multipleEmailsSchema.RawURL, + }, }, }) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/driver.go b/internal/driver.go index 3499a83b5b9b..5d31cfe5ceb2 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -84,7 +84,7 @@ func NewFastRegistryWithMocks(t *testing.T, opts ...configx.OptionModifier) (*co func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { ctx := context.Background() c := NewConfigurationWithDefaults(t, append(opts, configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)), + config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)+"&lock=false&max_conns=1"), "dev": true, }))...) reg, err := driver.NewRegistryFromDSN(ctx, c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel))) diff --git a/persistence/sql/persister_cleanup_test.go b/persistence/sql/persister_cleanup_test.go index 65e95ea6ea00..efb14a05e6c9 100644 --- a/persistence/sql/persister_cleanup_test.go +++ b/persistence/sql/persister_cleanup_test.go @@ -14,6 +14,8 @@ import ( ) func TestPersister_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() ctx := context.Background() @@ -29,6 +31,8 @@ func TestPersister_Cleanup(t *testing.T) { } func TestPersister_Continuity_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -45,6 +49,8 @@ func TestPersister_Continuity_Cleanup(t *testing.T) { } func TestPersister_Login_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -61,6 +67,8 @@ func TestPersister_Login_Cleanup(t *testing.T) { } func TestPersister_Recovery_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -77,6 +85,8 @@ func TestPersister_Recovery_Cleanup(t *testing.T) { } func TestPersister_Registration_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -93,6 +103,8 @@ func TestPersister_Registration_Cleanup(t *testing.T) { } func TestPersister_Session_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -109,6 +121,8 @@ func TestPersister_Session_Cleanup(t *testing.T) { } func TestPersister_Settings_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -125,6 +139,8 @@ func TestPersister_Settings_Cleanup(t *testing.T) { } func TestPersister_Verification_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -141,6 +157,8 @@ func TestPersister_Verification_Cleanup(t *testing.T) { } func TestPersister_SessionTokenExchange_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() diff --git a/persistence/sql/persister_code.go b/persistence/sql/persister_code.go index 31e0b80dc2d2..ece7dea75ec3 100644 --- a/persistence/sql/persister_code.go +++ b/persistence/sql/persister_code.go @@ -94,7 +94,7 @@ func useOneTimeCode[P any, U interface { secrets: for _, secret := range p.r.Config().SecretsSession(ctx) { - suppliedCode := []byte(p.hmacValueWithSecret(ctx, userProvidedCode, secret)) + suppliedCode := []byte(hmacValueWithSecret(ctx, userProvidedCode, secret)) for i := range codes { c := codes[i] if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 { diff --git a/persistence/sql/persister_errorx.go b/persistence/sql/persister_errorx.go index 15faf9fd163b..fc656074f0fc 100644 --- a/persistence/sql/persister_errorx.go +++ b/persistence/sql/persister_errorx.go @@ -4,18 +4,17 @@ package sql import ( - "bytes" "context" "encoding/json" "time" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/pkg/errors" "go.opentelemetry.io/otel/attribute" - "github.com/ory/jsonschema/v3" - "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" "github.com/ory/x/otelx" "github.com/ory/x/sqlcon" @@ -28,7 +27,7 @@ func (p *Persister) CreateErrorContainer(ctx context.Context, csrfToken string, ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateErrorContainer") defer otelx.End(span, &err) - message, err := p.encodeSelfServiceErrors(ctx, errs) + message, err := encodeSelfServiceErrors(errs) if err != nil { return uuid.Nil, err } @@ -55,14 +54,19 @@ func (p *Persister) ReadErrorContainer(ctx context.Context, id uuid.UUID) (_ *er defer otelx.End(span, &err) var ec errorx.ErrorContainer - if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil { - return nil, sqlcon.HandleError(err) - } - - if err := p.GetConnection(ctx).RawQuery( - "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?", - time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil { - return nil, sqlcon.HandleError(err) + if err := p.Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := c.Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil { + return sqlcon.HandleError(err) + } + + if err := c.RawQuery( + "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?", + time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil { + return sqlcon.HandleError(err) + } + return nil + }); err != nil { + return nil, err } return &ec, nil @@ -85,7 +89,7 @@ func (p *Persister) ClearErrorContainers(ctx context.Context, olderThan time.Dur return sqlcon.HandleError(err) } -func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byte, error) { +func encodeSelfServiceErrors(e error) ([]byte, error) { if e == nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithDebug("A nil error was passed to the error manager which is most likely a code bug.")) } @@ -98,10 +102,10 @@ func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byt e = herodot.ToDefaultError(e, "") } - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(e); err != nil { + enc, err := json.Marshal(e) + if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode error messages.").WithDebug(err.Error())) } - return b.Bytes(), nil + return enc, nil } diff --git a/persistence/sql/persister_hmac.go b/persistence/sql/persister_hmac.go index 9c4d6636f14c..8fdb04df3dd6 100644 --- a/persistence/sql/persister_hmac.go +++ b/persistence/sql/persister_hmac.go @@ -7,27 +7,19 @@ import ( "context" "crypto/hmac" "crypto/sha512" - "crypto/subtle" "fmt" + + "go.opentelemetry.io/otel/trace" ) func (p *Persister) hmacValue(ctx context.Context, value string) string { - return p.hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0]) + return hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0]) } -func (p *Persister) hmacValueWithSecret(ctx context.Context, value string, secret []byte) string { - _, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.hmacValueWithSecret") +func hmacValueWithSecret(ctx context.Context, value string, secret []byte) string { + _, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "persistence.sql.hmacValueWithSecret") defer span.End() h := hmac.New(sha512.New512_256, secret) _, _ = h.Write([]byte(value)) return fmt.Sprintf("%x", h.Sum(nil)) } - -func (p *Persister) hmacConstantCompare(ctx context.Context, value, hash string) bool { - for _, secret := range p.r.Config().SecretsSession(ctx) { - if subtle.ConstantTimeCompare([]byte(p.hmacValueWithSecret(ctx, value, secret)), []byte(hash)) == 1 { - return true - } - } - return false -} diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index 7b8cc8575368..c569affa0cd9 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -8,9 +8,10 @@ import ( "os" "testing" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" - "github.com/ory/x/configx" "github.com/ory/x/otelx" "github.com/gobuffalo/pop/v6" @@ -49,10 +50,10 @@ func (l *logRegistryOnly) Audit() *logrusx.Logger { panic("implement me") } -func (l *logRegistryOnly) Tracer(ctx context.Context) *otelx.Tracer { +func (l *logRegistryOnly) Tracer(context.Context) *otelx.Tracer { return otelx.NewNoop(l.l, new(otelx.Config)) } -func (l *logRegistryOnly) IdentityTraitsSchemas(ctx context.Context) (schema.IdentitySchemaList, error) { +func (l *logRegistryOnly) IdentityTraitsSchemas(context.Context) (schema.IdentitySchemaList, error) { panic("implement me") } @@ -63,25 +64,36 @@ func (l *logRegistryOnly) IdentityValidator() *identity.Validator { var _ persisterDependencies = &logRegistryOnly{} func TestPersisterHMAC(t *testing.T) { + t.Parallel() + ctx := context.Background() - conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"foobarbaz"}) + baseSecret := "foobarbaz" + baseSecretBytes := []byte(baseSecret) + opts := []configx.OptionModifier{configx.SkipValidation(), configx.WithValue(config.ViperKeySecretsDefault, []string{baseSecret})} + conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...) c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"}) require.NoError(t, err) - p, err := NewPersister(context.Background(), &logRegistryOnly{c: conf}, c) + p, err := NewPersister(ctx, &logRegistryOnly{c: conf}, c) require.NoError(t, err) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - assert.False(t, p.hmacConstantCompare(context.Background(), "notme", p.hmacValue(context.Background(), "hashme"))) - assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "notme"))) - - hash := p.hmacValue(context.Background(), "hashme") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz"}) - assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", hash)) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz", "foobarbaz"}) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", hash)) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - assert.NotEqual(t, hash, p.hmacValue(context.Background(), "hashme")) + t.Run("case=behaves deterministically", func(t *testing.T) { + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret(ctx, "notme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "notme")) + }) + + hash := p.hmacValue(ctx, "hashme") + newSecret := "not" + baseSecret + + t.Run("case=with only new sectet", func(t *testing.T) { + ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) + assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + }) + + t.Run("case=with new and old secret", func(t *testing.T) { + ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hash, p.hmacValue(ctx, "hashme")) + }) } diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go index 468ba5a2b144..bb23d3fd319e 100644 --- a/persistence/sql/persister_recovery.go +++ b/persistence/sql/persister_recovery.go @@ -82,7 +82,7 @@ func (p *Persister) UseRecoveryToken(ctx context.Context, fID uuid.UUID, token s nid := p.NetworkID(ctx) if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { for _, secret := range p.r.Config().SecretsSession(ctx) { - if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { + if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) { return err } diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go index f88d7380a5c7..6a48e763d0f3 100644 --- a/persistence/sql/persister_test.go +++ b/persistence/sql/persister_test.go @@ -94,7 +94,7 @@ func pl(t testing.TB) func(lvl logging.Level, s string, args ...interface{}) { func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault { conns := map[string]string{ - "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true", + "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true&max_conns=1&lock=false", } var l sync.Mutex @@ -160,111 +160,104 @@ func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault { } func TestPersister(t *testing.T) { + t.Parallel() + conns := createCleanDatabases(t) - ctx := context.Background() + ctx := testhelpers.WithDefaultIdentitySchema(context.Background(), "file://./stub/identity.schema.json") - for name := range conns { - name := name - reg := conns[name] + for name, reg := range conns { t.Run(fmt.Sprintf("database=%s", name), func(t *testing.T) { t.Parallel() _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) - conf := reg.Config() - t.Logf("DSN: %s", conf.DSN(ctx)) + t.Logf("DSN: %s", reg.Config().DSN(ctx)) - // This test must remain the first test in the test suite! t.Run("racy identity creation", func(t *testing.T) { - defaultSchema := schema.Schema{ - ID: config.DefaultIdentityTraitsSchemaID, - URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"), - RawURL: "file://./stub/identity.schema.json", - } + t.Parallel() var wg sync.WaitGroup - testhelpers.SetDefaultIdentitySchema(reg.Config(), defaultSchema.RawURL) + _, ps := testhelpers.NewNetwork(t, ctx, reg.Persister()) - for i := 0; i < 10; i++ { + for i := range 10 { wg.Add(1) - // capture i - ii := i go func() { defer wg.Done() id := ri.NewIdentity("") id.SetCredentials(ri.CredentialsTypePassword, ri.Credentials{ Type: ri.CredentialsTypePassword, - Identifiers: []string{fmt.Sprintf("racy identity %d", ii)}, + Identifiers: []string{fmt.Sprintf("racy identity %d", i)}, Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), }) id.Traits = ri.Traits("{}") - require.NoError(t, ps.CreateIdentity(context.Background(), id)) + require.NoError(t, ps.CreateIdentity(ctx, id)) }() } wg.Wait() }) - t.Run("case=credentials types", func(t *testing.T) { + t.Run("case=credential types exist", func(t *testing.T) { + t.Parallel() for _, ct := range []ri.CredentialsType{ri.CredentialsTypeOIDC, ri.CredentialsTypePassword} { require.NoError(t, p.(*sql.Persister).Connection(context.Background()).Where("name = ?", ct).First(&ri.CredentialsTypeTable{})) } }) t.Run("contract=identity.TestPool", func(t *testing.T) { - pop.SetLogger(pl(t)) - identity.TestPool(ctx, conf, p, reg.IdentityManager(), name)(t) + t.Parallel() + identity.TestPool(ctx, p, reg.IdentityManager(), name)(t) }) t.Run("contract=registration.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() registration.TestFlowPersister(ctx, p)(t) }) t.Run("contract=errorx.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() errorx.TestPersister(ctx, p)(t) }) t.Run("contract=login.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() login.TestFlowPersister(ctx, p)(t) }) t.Run("contract=settings.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - settings.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + settings.TestFlowPersister(ctx, p)(t) }) t.Run("contract=session.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - session.TestPersister(ctx, conf, p)(t) + t.Parallel() + session.TestPersister(ctx, reg.Config(), p)(t) }) t.Run("contract=sessiontokenexchange.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - sessiontokenexchange.TestPersister(ctx, conf, p)(t) + t.Parallel() + sessiontokenexchange.TestPersister(ctx, p)(t) }) t.Run("contract=courier.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() upsert, insert := sqltesthelpers.DefaultNetworkWrapper(p) courier.TestPersister(ctx, upsert, insert)(t) }) t.Run("contract=verification.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - verification.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + verification.TestFlowPersister(ctx, p)(t) }) t.Run("contract=recovery.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - recovery.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + recovery.TestFlowPersister(ctx, p)(t) }) t.Run("contract=link.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - link.TestPersister(ctx, conf, p)(t) + t.Parallel() + link.TestPersister(ctx, p)(t) }) t.Run("contract=code.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - code.TestPersister(ctx, conf, p)(t) + t.Parallel() + code.TestPersister(ctx, p)(t) }) t.Run("contract=continuity.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() continuity.TestPersister(ctx, p)(t) }) }) @@ -283,6 +276,8 @@ func getErr(args ...interface{}) error { } func TestPersister_Transaction(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go index 8d983ed1635d..7feae0592ae7 100644 --- a/persistence/sql/persister_verification.go +++ b/persistence/sql/persister_verification.go @@ -82,7 +82,7 @@ func (p *Persister) UseVerificationToken(ctx context.Context, fID uuid.UUID, tok nid := p.NetworkID(ctx) if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { for _, secret := range p.r.Config().SecretsSession(ctx) { - if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { + if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) { return err } diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go index 8bc9efad88e0..75dd6b00c6b2 100644 --- a/selfservice/flow/recovery/test/persistence.go +++ b/selfservice/flow/recovery/test/persistence.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -23,7 +22,7 @@ import ( "github.com/ory/x/sqlcon" ) -func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { +func TestFlowPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { @@ -33,7 +32,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") t.Run("case=should error when the recovery request does not exist", func(t *testing.T) { _, err := p.GetRecoveryFlow(ctx, x.NewUUID()) diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go index 85c80e49d74e..498419a65c3a 100644 --- a/selfservice/flow/settings/test/persistence.go +++ b/selfservice/flow/settings/test/persistence.go @@ -27,7 +27,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/x" ) @@ -37,12 +36,10 @@ func clearids(r *settings.Flow) { r.IdentityID = uuid.Nil } -func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.Persister) func(t *testing.T) { +func TestFlowPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - t.Run("case=should error when the settings request does not exist", func(t *testing.T) { _, err := p.GetSettingsFlow(ctx, x.NewUUID()) require.Error(t, err) diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go index 57c35cba8d2e..a021d06a152e 100644 --- a/selfservice/flow/verification/test/persistence.go +++ b/selfservice/flow/verification/test/persistence.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -23,7 +22,7 @@ import ( "github.com/ory/x/sqlcon" ) -func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { +func TestFlowPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { @@ -34,8 +33,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - t.Run("case=should error when the verification request does not exist", func(t *testing.T) { _, err := p.GetVerificationFlow(ctx, x.NewUUID()) require.Error(t, err) diff --git a/selfservice/sessiontokenexchange/test/persistence.go b/selfservice/sessiontokenexchange/test/persistence.go index 53db63db04f3..da19c3edc1a3 100644 --- a/selfservice/sessiontokenexchange/test/persistence.go +++ b/selfservice/sessiontokenexchange/test/persistence.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/sessiontokenexchange" @@ -36,11 +35,10 @@ func (t *testParams) setCodes(e *sessiontokenexchange.Exchanger) { t.returnToCode = e.ReturnToCode } -func TestPersister(ctx context.Context, _ *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }) func(t *testing.T) { return func(t *testing.T) { - t.Parallel() nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) t.Run("suite=create-update-get", func(t *testing.T) { diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index f3c120402ddb..e7648cb055b3 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -24,15 +24,14 @@ import ( "github.com/ory/kratos/x" ) -func TestPersister(ctx context.Context, conf *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("code=recovery", func(t *testing.T) { newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) { diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index af5738eaae31..a77a1db1d9c8 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -28,15 +28,14 @@ import ( "github.com/ory/kratos/x" ) -func TestPersister(ctx context.Context, conf *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("token=recovery", func(t *testing.T) { newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) { diff --git a/session/test/persistence.go b/session/test/persistence.go index 0db6964468d8..0b709a3866b8 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -42,8 +42,6 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - ctx := testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/identity.schema.json") - t.Run("case=not found", func(t *testing.T) { _, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing) require.Error(t, err) From bac030b3d5a4d145fb616d59d1603bb2c5f2a429 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:25:54 +0000 Subject: [PATCH 122/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From e0001b0db784457652581366bd7ead7cdf6b3898 Mon Sep 17 00:00:00 2001 From: Patrik Date: Tue, 18 Jun 2024 11:18:08 +0200 Subject: [PATCH 123/262] test: enable server-side config from context (#3954) --- cipher/cipher_test.go | 8 +- driver/config/handler_test.go | 40 +++-- driver/config/test_config.go | 64 -------- driver/config/testhelpers/config.go | 152 ++++++++++++++++++ driver/registry_default_test.go | 36 +++-- identity/test/pool.go | 4 +- internal/driver.go | 6 +- internal/testhelpers/config.go | 6 +- persistence/sql/persister_hmac_test.go | 8 +- selfservice/strategy/code/test/persistence.go | 6 +- selfservice/strategy/link/test/persistence.go | 4 +- session/test/persistence.go | 8 +- 12 files changed, 234 insertions(+), 108 deletions(-) delete mode 100644 driver/config/test_config.go create mode 100644 driver/config/testhelpers/config.go diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go index eb8ba7e1ba7b..90e02ff0de45 100644 --- a/cipher/cipher_test.go +++ b/cipher/cipher_test.go @@ -9,6 +9,8 @@ import ( "fmt" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/configx" "github.com/stretchr/testify/assert" @@ -44,7 +46,7 @@ func TestCipher(t *testing.T) { t.Run("case=encryption_failed", func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) // secret have to be set _, err := c.Encrypt(ctx, []byte("not-empty")) @@ -53,7 +55,7 @@ func TestCipher(t *testing.T) { require.ErrorAs(t, err, &hErr) assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) - ctx = config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) // bad secret length _, err = c.Encrypt(ctx, []byte("not-empty")) @@ -70,7 +72,7 @@ func TestCipher(t *testing.T) { _, err = c.Decrypt(ctx, "not-empty") require.Error(t, err) - _, err = c.Decrypt(config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") + _, err = c.Decrypt(confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") require.Error(t, err) }) }) diff --git a/driver/config/handler_test.go b/driver/config/handler_test.go index da84a5bc08d4..8c73a9319621 100644 --- a/driver/config/handler_test.go +++ b/driver/config/handler_test.go @@ -6,9 +6,10 @@ package config_test import ( "context" "io" - "net/http/httptest" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,21 +18,32 @@ import ( "github.com/ory/kratos/internal" ) +type configProvider struct { + cfg *config.Config +} + +func (c *configProvider) Config() *config.Config { + return c.cfg +} + func TestNewConfigHashHandler(t *testing.T) { ctx := context.Background() - conf, reg := internal.NewFastRegistryWithMocks(t) + cfg := internal.NewConfigurationWithDefaults(t) router := httprouter.New() - config.NewConfigHashHandler(reg, router) - ts := httptest.NewServer(router) + config.NewConfigHashHandler(&configProvider{cfg: cfg}, router) + ts := confighelpers.NewConfigurableTestServer(router) t.Cleanup(ts.Close) - res, err := ts.Client().Get(ts.URL + "/health/config") + + // first request, get baseline hash + res, err := ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) first, err := io.ReadAll(res.Body) require.NoError(t, err) - res, err = ts.Client().Get(ts.URL + "/health/config") + // second request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) @@ -39,13 +51,21 @@ func TestNewConfigHashHandler(t *testing.T) { require.NoError(t, err) assert.Equal(t, first, second) - require.NoError(t, conf.Set(ctx, config.ViperKeySessionDomain, "foobar")) + // third request, with config change + res, err = ts.Client(confighelpers.WithConfigValue(ctx, config.ViperKeySessionDomain, "foobar")).Get(ts.URL + "/health/config") + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, 200, res.StatusCode) + third, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.NotEqual(t, first, third) - res, err = ts.Client().Get(ts.URL + "/health/config") + // fourth request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) - second, err = io.ReadAll(res.Body) + fourth, err := io.ReadAll(res.Body) require.NoError(t, err) - assert.NotEqual(t, first, second) + assert.Equal(t, first, fourth) } diff --git a/driver/config/test_config.go b/driver/config/test_config.go deleted file mode 100644 index c95fba7b7876..000000000000 --- a/driver/config/test_config.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright © 2024 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "context" - - "github.com/ory/kratos/embedx" - "github.com/ory/x/configx" - "github.com/ory/x/contextx" -) - -type ( - TestConfigProvider struct { - contextx.Contextualizer - Options []configx.OptionModifier - } - contextKey int -) - -func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { - return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...) -} - -func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { - config = t.Contextualizer.Config(ctx, config) - values, ok := ctx.Value(contextConfigKey).([]map[string]any) - if !ok { - return config - } - opts := make([]configx.OptionModifier, 0, len(values)) - for _, v := range values { - opts = append(opts, configx.WithValues(v)) - } - config, err := t.NewProvider(ctx, opts...) - if err != nil { - // This is not production code. The provider is only used in tests. - panic(err) - } - return config -} - -const contextConfigKey contextKey = 1 - -var ( - _ contextx.Contextualizer = (*TestConfigProvider)(nil) -) - -func WithConfigValue(ctx context.Context, key string, value any) context.Context { - return WithConfigValues(ctx, map[string]any{key: value}) -} - -func WithConfigValues(ctx context.Context, setValues map[string]any) context.Context { - values, ok := ctx.Value(contextConfigKey).([]map[string]any) - if !ok { - values = make([]map[string]any, 0) - } - newValues := make([]map[string]any, len(values), len(values)+1) - copy(newValues, values) - newValues = append(newValues, setValues) - - return context.WithValue(ctx, contextConfigKey, newValues) -} diff --git a/driver/config/testhelpers/config.go b/driver/config/testhelpers/config.go new file mode 100644 index 000000000000..6d0e4ba0b910 --- /dev/null +++ b/driver/config/testhelpers/config.go @@ -0,0 +1,152 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "net/http" + "net/http/httptest" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/embedx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" +) + +type ( + TestConfigProvider struct { + contextx.Contextualizer + Options []configx.OptionModifier + } + contextKey int +) + +func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { + return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...) +} + +func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + config = t.Contextualizer.Config(ctx, config) + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + return config + } + opts := make([]configx.OptionModifier, 0, len(values)) + for _, v := range values { + opts = append(opts, configx.WithValues(v)) + } + config, err := t.NewProvider(ctx, opts...) + if err != nil { + // This is not production code. The provider is only used in tests. + panic(err) + } + return config +} + +const contextConfigKey contextKey = 1 + +var ( + _ contextx.Contextualizer = (*TestConfigProvider)(nil) +) + +func WithConfigValue(ctx context.Context, key string, value any) context.Context { + return WithConfigValues(ctx, map[string]any{key: value}) +} + +func WithConfigValues(ctx context.Context, setValues ...map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + values = make([]map[string]any, 0) + } + newValues := make([]map[string]any, len(values), len(values)+len(setValues)) + copy(newValues, values) + newValues = append(newValues, setValues...) + + return context.WithValue(ctx, contextConfigKey, newValues) +} + +type ConfigurableTestHandler struct { + configs map[uuid.UUID][]map[string]any + handler http.Handler +} + +func NewConfigurableTestHandler(h http.Handler) *ConfigurableTestHandler { + return &ConfigurableTestHandler{ + configs: make(map[uuid.UUID][]map[string]any), + handler: h, + } +} + +func (t *ConfigurableTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cID := r.Header.Get("Test-Config-Id") + if config, ok := t.configs[uuid.FromStringOrNil(cID)]; ok { + r = r.WithContext(WithConfigValues(r.Context(), config...)) + } + t.handler.ServeHTTP(w, r) +} + +func (t *ConfigurableTestHandler) RegisterConfig(config ...map[string]any) uuid.UUID { + id := uuid.Must(uuid.NewV4()) + t.configs[id] = config + return id +} + +func (t *ConfigurableTestHandler) UseConfig(r *http.Request, id uuid.UUID) *http.Request { + r.Header.Set("Test-Config-Id", id.String()) + return r +} + +func (t *ConfigurableTestHandler) UseConfigValues(r *http.Request, values ...map[string]any) *http.Request { + return t.UseConfig(r, t.RegisterConfig(values...)) +} + +type ConfigurableTestServer struct { + *httptest.Server + handler *ConfigurableTestHandler + transport http.RoundTripper +} + +func NewConfigurableTestServer(h http.Handler) *ConfigurableTestServer { + handler := NewConfigurableTestHandler(h) + server := httptest.NewServer(handler) + + t := server.Client().Transport + cts := &ConfigurableTestServer{ + handler: handler, + Server: server, + transport: t, + } + server.Client().Transport = cts + return cts +} + +func (t *ConfigurableTestServer) RoundTrip(r *http.Request) (*http.Response, error) { + config, ok := r.Context().Value(contextConfigKey).([]map[string]any) + if ok && config != nil { + r = t.handler.UseConfigValues(r, config...) + } + return t.transport.RoundTrip(r) +} + +type AutoContextClient struct { + *http.Client + transport http.RoundTripper + ctx context.Context +} + +func (t *ConfigurableTestServer) Client(ctx context.Context) *AutoContextClient { + baseClient := *t.Server.Client() + autoClient := &AutoContextClient{ + Client: &baseClient, + transport: t, + ctx: ctx, + } + baseClient.Transport = autoClient + return autoClient +} + +func (c *AutoContextClient) RoundTrip(r *http.Request) (*http.Response, error) { + return c.transport.RoundTrip(r.WithContext(c.ctx)) +} diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 020517a41159..fa3e7772c62a 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -10,6 +10,8 @@ import ( "os" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/contextx" "github.com/ory/kratos/selfservice/flow/recovery" @@ -69,7 +71,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreVerificationHooks(ctx) @@ -110,7 +112,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostVerificationHooks(ctx) @@ -152,7 +154,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreRecoveryHooks(ctx) @@ -191,7 +193,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostRecoveryHooks(ctx) @@ -238,7 +240,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreRegistrationHooks(ctx) @@ -342,7 +344,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) @@ -384,7 +386,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreLoginHooks(ctx) @@ -486,7 +488,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword) @@ -528,7 +530,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreSettingsHooks(ctx) @@ -614,7 +616,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostSettingsPostPersistHooks(ctx, "profile") @@ -685,7 +687,7 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) s := reg.RegistrationStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { @@ -758,7 +760,7 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run(fmt.Sprintf("run=%s", tc.name), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) s := reg.LoginStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { @@ -790,7 +792,7 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) s := reg.RecoveryStrategies(ctx) require.Len(t, s, len(tc.expect)) @@ -912,7 +914,7 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ "selfservice.methods.code.enabled": false, config.ViperKeySelfServiceRecoveryUse: "code", }) @@ -926,7 +928,7 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, config.ViperKeySelfServiceRecoveryUse: sID, }) @@ -944,7 +946,7 @@ func TestGetActiveVerificationStrategy(t *testing.T) { ctx := context.Background() _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ "selfservice.methods.code.enabled": false, config.ViperKeySelfServiceVerificationUse: "code", }) @@ -957,7 +959,7 @@ func TestGetActiveVerificationStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, config.ViperKeySelfServiceVerificationUse: sID, }) diff --git a/identity/test/pool.go b/identity/test/pool.go index bf6d114b8510..458c057da916 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -13,6 +13,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/crdbx" "github.com/go-faker/faker/v4" @@ -61,7 +63,7 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager, URL: urlx.ParseOrPanic("file://./stub/handler/multiple_emails.schema.json"), RawURL: "file://./stub/identity-2.schema.json", } - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ config.ViperKeyPublicBaseURL: exampleServerURL.String(), config.ViperKeyIdentitySchemas: []config.Schema{ { diff --git a/internal/driver.go b/internal/driver.go index 5d31cfe5ceb2..b95b2dc7c0a9 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -9,6 +9,8 @@ import ( "runtime" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/contextx" "github.com/sirupsen/logrus" @@ -56,7 +58,7 @@ func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) }, opts...) c := config.MustNew(t, logrusx.New("", ""), os.Stderr, - &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts}, + &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts}, configOpts..., ) return c @@ -91,7 +93,7 @@ func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionM require.NoError(t, err) pool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) t.Cleanup(pool.Close) - require.NoError(t, reg.Init(context.Background(), &config.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) + require.NoError(t, reg.Init(context.Background(), &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) require.NoError(t, reg.Persister().MigrateUp(context.Background())) // always migrate up actual, err := reg.Persister().DetermineNetwork(context.Background()) diff --git a/internal/testhelpers/config.go b/internal/testhelpers/config.go index 8e17a6ab3a12..b3450bda72fd 100644 --- a/internal/testhelpers/config.go +++ b/internal/testhelpers/config.go @@ -8,6 +8,8 @@ import ( "encoding/base64" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/spf13/pflag" "github.com/stretchr/testify/require" @@ -33,7 +35,7 @@ func DefaultIdentitySchemaConfig(url string) map[string]any { } func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context { - return config.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url)) + return confighelpers.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url)) } // Deprecated: Use context-based WithDefaultIdentitySchema instead @@ -58,7 +60,7 @@ func WithAddIdentitySchema(ctx context.Context, t *testing.T, conf *config.Confi schemas, err := conf.IdentityTraitsSchemas(ctx) require.NoError(t, err) - return config.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ + return confighelpers.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ ID: id, URL: url, })), id diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index c569affa0cd9..fa1d6e479308 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -8,6 +8,8 @@ import ( "os" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/configx" "github.com/ory/x/contextx" @@ -70,7 +72,7 @@ func TestPersisterHMAC(t *testing.T) { baseSecret := "foobarbaz" baseSecretBytes := []byte(baseSecret) opts := []configx.OptionModifier{configx.SkipValidation(), configx.WithValue(config.ViperKeySecretsDefault, []string{baseSecret})} - conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...) + conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...) c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"}) require.NoError(t, err) p, err := NewPersister(ctx, &logRegistryOnly{c: conf}, c) @@ -86,13 +88,13 @@ func TestPersisterHMAC(t *testing.T) { newSecret := "not" + baseSecret t.Run("case=with only new sectet", func(t *testing.T) { - ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) }) t.Run("case=with new and old secret", func(t *testing.T) { - ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) assert.NotEqual(t, hash, p.hmacValue(ctx, "hashme")) }) diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index e7648cb055b3..d7600e9fbd9a 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -31,7 +33,7 @@ func TestPersister(ctx context.Context, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("code=recovery", func(t *testing.T) { newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) { @@ -50,7 +52,7 @@ func TestPersister(ctx context.Context, p interface { require.NoError(t, p.CreateIdentity(ctx, &i)) return &code.CreateRecoveryCodeParams{ - RawCode: string(randx.MustString(8, randx.Numeric)), + RawCode: randx.MustString(8, randx.Numeric), FlowID: f.ID, RecoveryAddress: &i.RecoveryAddresses[0], ExpiresIn: time.Minute, diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index a77a1db1d9c8..c28250836775 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -35,7 +37,7 @@ func TestPersister(ctx context.Context, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("token=recovery", func(t *testing.T) { newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) { diff --git a/session/test/persistence.go b/session/test/persistence.go index 0b709a3866b8..d124aa23eb4f 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -609,7 +611,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) + ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) var expected session.Session require.NoError(t, faker.FakeData(&expected)) @@ -624,7 +626,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) + ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) var expected session.Session require.NoError(t, faker.FakeData(&expected)) @@ -644,7 +646,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { t.Skip("Skipping test because driver is not CockroachDB") } - ctx := config.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour) var expected session.Session require.NoError(t, faker.FakeData(&expected)) From af5ea35759e74d7a1637823abcc21dc8e3e39a9d Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:02:49 +0200 Subject: [PATCH 124/262] feat: clarify session extend behavior (#3962) --- internal/client-go/api_identity.go | 6 ++++++ internal/httpclient/api_identity.go | 6 ++++++ session/handler.go | 3 +++ spec/api.json | 2 +- spec/swagger.json | 2 +- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index 62f5f80b36c2..b1201db57750 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -161,6 +161,9 @@ type IdentityApi interface { return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. + This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those + scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. @@ -1494,6 +1497,9 @@ This endpoint returns per default a 204 No Content response on success. Older Or return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. +This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index 62f5f80b36c2..b1201db57750 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -161,6 +161,9 @@ type IdentityApi interface { return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. + This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those + scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. @@ -1494,6 +1497,9 @@ This endpoint returns per default a 204 No Content response on success. Older Or return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. +This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. diff --git a/session/handler.go b/session/handler.go index 9dab8860c773..84be9c25e1b8 100644 --- a/session/handler.go +++ b/session/handler.go @@ -877,6 +877,9 @@ type extendSession struct { // return a 200 OK response with the session in the body. Returning the session as part of the response // will be deprecated in the future and should not be relied upon. // +// This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +// scenarios. This endpoint also returns 404 errors if the session does not exist. +// // Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. // // Schemes: http, https diff --git a/spec/api.json b/spec/api.json index 084bbfa5ea47..582e44a779f1 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4969,7 +4969,7 @@ }, "/admin/sessions/{id}/extend": { "patch": { - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "operationId": "extendSession", "parameters": [ { diff --git a/spec/swagger.json b/spec/swagger.json index 8ccd39801919..e77bfefa6307 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1188,7 +1188,7 @@ "oryAccessToken": [] } ], - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "schemes": [ "http", "https" From a43cef23c177acddbf8b03afef087feeaca51981 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 25 Jun 2024 11:21:17 +0200 Subject: [PATCH 125/262] feat: allow deletion of an individual OIDC credential (#3968) This extends the existing `DELETE /admin/identities/{id}/credentials/{type}` API to accept an `?identifier=foobar` query parameter for `{type}==oidc` like such: `DELETE /admin/identities/{id}/credentials/oidc?identifier=github%3A012345` This will delete the GitHub OIDC credential with the identifier `github:012345` (`012345` is the subject as returned by GitHub). To find out which OIDC credentials exist, call `GET /admin/identities/{id}?include_credential=oidc` beforehand. This will allow you to delete individual OIDC credentials for users even if they have several set up. --- identity/handler.go | 67 ++++++---------------- identity/handler_test.go | 118 ++++++++++++++++++++++++++++---------- identity/identity.go | 74 ++++++++++++++++++++++++ identity/identity_test.go | 72 +++++++++++++++++++---- 4 files changed, 243 insertions(+), 88 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index 3ac641e09377..cf4d3ff835e6 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -910,69 +910,36 @@ func (h *Handler) patch(w http.ResponseWriter, r *http.Request, ps httprouter.Pa h.r.Writer().Write(w, r, WithCredentialsMetadataAndAdminMetadataInJSON(updatedIdentity)) } -func deletCredentialWebAuthFromIdentity(identity *Identity) (*Identity, error) { - cred, ok := identity.GetCredentials(CredentialsTypeWebAuthn) - if !ok { - // This should never happend as it's checked earlier in the code; - // But we never know... - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a CredentialsTypeWebAuthn but this user have no CredentialsTypeWebAuthn set up.")) - } - - var cc CredentialsWebAuthnConfig - if err := json.Unmarshal(cred.Config, &cc); err != nil { - // Database has been tampered or the json schema are incompatible (migration issue); - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) - } - - updated := make([]CredentialWebAuthn, 0) - for k, cred := range cc.Credentials { - if cred.IsPasswordless { - updated = append(updated, cc.Credentials[k]) - } - } - - if len(updated) == 0 { - identity.DeleteCredentialsType(CredentialsTypeWebAuthn) - return identity, nil - } - - cc.Credentials = updated - message, err := json.Marshal(cc) - if err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) - } - - cred.Config = message - identity.SetCredentials(CredentialsTypeWebAuthn, *cred) - return identity, nil -} - // Delete Credential Parameters // // swagger:parameters deleteIdentityCredentials -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type deleteIdentityCredentials struct { +type _ struct { // ID is the identity's ID. // // required: true // in: path ID string `json:"id"` - // Type is the type of credentials to be deleted. + // Type is the type of credentials to delete. // // required: true // in: path Type CredentialsType `json:"type"` + + // Identifier is the identifier of the OIDC credential to delete. + // Find the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint. + // + // required: false + // in: query + Identifier string `json:"identifier"` } // swagger:route DELETE /admin/identities/{id}/credentials/{type} identity deleteIdentityCredentials // // # Delete a credential for a specific identity // -// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type -// You can only delete second factor (aal2) credentials. +// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. +// You cannot delete password or code auth credentials through this API. // // Consumes: // - application/json @@ -1006,14 +973,18 @@ func (h *Handler) deleteIdentityCredentials(w http.ResponseWriter, r *http.Reque case CredentialsTypeLookup, CredentialsTypeTOTP: identity.DeleteCredentialsType(cred.Type) case CredentialsTypeWebAuthn: - identity, err = deletCredentialWebAuthFromIdentity(identity) - if err != nil { + if err = identity.deleteCredentialWebAuthFromIdentity(); err != nil { h.r.Writer().WriteError(w, r, err) return } - case CredentialsTypeOIDC, CredentialsTypePassword, CredentialsTypeCodeAuth: - h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You can't remove first factor credentials."))) + case CredentialsTypePassword, CredentialsTypeCodeAuth: + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You cannot remove first factor credentials."))) return + case CredentialsTypeOIDC: + if err := identity.deleteCredentialOIDCFromIdentity(r.URL.Query().Get("identifier")); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } default: h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unknown credentials type %s.", cred.Type))) return diff --git a/identity/handler_test.go b/identity/handler_test.go index ab2ed0c0cbdc..53ca219b231b 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -32,6 +32,8 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/schema" "github.com/ory/kratos/x" + "github.com/ory/x/ioutilx" + "github.com/ory/x/randx" "github.com/ory/x/snapshotx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" @@ -81,8 +83,9 @@ func TestHandler(t *testing.T) { res, err := base.Client().Do(req) require.NoError(t, err) + defer res.Body.Close() - require.EqualValues(t, expectCode, res.StatusCode) + require.EqualValues(t, expectCode, res.StatusCode, "%s", ioutilx.MustReadAll(res.Body)) } send := func(t *testing.T, base *httptest.Server, method, href string, expectCode int, send interface{}) gjson.Result { @@ -1497,15 +1500,15 @@ func TestHandler(t *testing.T) { t.Run("case=should delete credential of a specific user and no longer be able to retrieve it", func(t *testing.T) { ignoreDefault := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at"} - createIdentity := func(identities map[identity.CredentialsType]string) func(t *testing.T) *identity.Identity { + type M = map[identity.CredentialsType]identity.Credentials + createIdentity := func(creds M) func(*testing.T) *identity.Identity { return func(t *testing.T) *identity.Identity { i := identity.NewIdentity("") - for ct, config := range identities { - i.SetCredentials(ct, identity.Credentials{ - Type: ct, - Config: sqlxx.JSONRawMessage(config), - }) + for k, v := range creds { + v.Type = k + creds[k] = v } + i.Credentials = creds i.Traits = identity.Traits("{}") require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) return i @@ -1516,26 +1519,83 @@ func TestHandler(t *testing.T) { remove(t, ts, "/identities/"+x.NewUUID().String()+"/credentials/azerty", http.StatusNotFound) }) t.Run("type=remove unknown type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + i := createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, })(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/azerty", http.StatusNotFound) }) t.Run("type=remove password type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + i := createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, })(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/password", http.StatusBadRequest) }) t.Run("type=remove oidc type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypeOIDC: `{"id":"pst"}`, + // force ordering among github identifiers + githubSubject := "0" + randx.MustString(7, randx.Numeric) + githubSubject2 := "1" + randx.MustString(7, randx.Numeric) + googleSubject := randx.MustString(8, randx.Numeric) + initialConfig := []byte(fmt.Sprintf(`{ + "providers": [ + { + "subject": %q, + "provider": "github" + }, + { + "subject": %q, + "provider": "github" + }, + { + "subject": %q, + "provider": "google" + } + ] + }`, githubSubject, githubSubject2, googleSubject)) + identifiers := []string{ + identity.OIDCUniqueID("github", githubSubject), + identity.OIDCUniqueID("github", githubSubject2), + identity.OIDCUniqueID("google", googleSubject), + } + i := createIdentity(M{ + identity.CredentialsTypeOIDC: { + Identifiers: identifiers, + Config: initialConfig, + }, })(t) - remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc", http.StatusBadRequest) + res := get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[1], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.2").String(), identifiers[2], "%s", res.Raw) + + oidConfig := gjson.Parse(res.Get("credentials.oidc.config").String()) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), githubSubject2, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.2.provider").String(), "google", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.2.subject").String(), googleSubject, "%s", res.Raw) + + remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc?identifier="+identifiers[1], http.StatusNoContent) + res = get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) + + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[2], "%s", res.Raw) + + oidConfig = gjson.Parse(res.Get("credentials.oidc.config").String()) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "google", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), googleSubject, "%s", res.Raw) }) t.Run("type=remove webauthn passwordless type/"+name, func(t *testing.T) { expected := `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":true,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}` - i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: expected})(t) + i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: []byte(expected)}})(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent) // Check that webauthn has not been deleted res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK) @@ -1608,7 +1668,7 @@ func TestHandler(t *testing.T) { message, err := json.Marshal(config) require.NoError(t, err) - i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: string(message)})(t) + i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: message}})(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent) // Check that webauthn has not been deleted res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK) @@ -1618,10 +1678,10 @@ func TestHandler(t *testing.T) { require.NoError(t, err) snapshotx.SnapshotT(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), snapshotx.ExceptNestedKeys(append(ignoreDefault, "hashed_password")...), snapshotx.ExceptPaths("credentials.oidc.identifiers")) }) - for ct, ctConf := range map[identity.CredentialsType]string{ - identity.CredentialsTypeLookup: `{"recovery_codes": [{"code": "aaa"}]}`, - identity.CredentialsTypeTOTP: `{"totp_url":"otpauth://totp/test"}`, - identity.CredentialsTypeWebAuthn: `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`, + for ct, ctConf := range map[identity.CredentialsType][]byte{ + identity.CredentialsTypeLookup: []byte(`{"recovery_codes": [{"code": "aaa"}]}`), + identity.CredentialsTypeTOTP: []byte(`{"totp_url":"otpauth://totp/test"}`), + identity.CredentialsTypeWebAuthn: []byte(`{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`), } { t.Run("type=remove "+string(ct)+"/"+name, func(t *testing.T) { for _, tc := range []struct { @@ -1632,25 +1692,25 @@ func TestHandler(t *testing.T) { { desc: "with", exist: true, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, - ct: ctConf, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, + ct: {Config: ctConf}, }), }, { desc: "without", exist: false, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, }), }, { desc: "multiple", exist: true, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, - identity.CredentialsTypeOIDC: `{"id":"pst"}`, - ct: ctConf, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, + identity.CredentialsTypeOIDC: {Config: []byte(`{"id":"pst"}`)}, + ct: {Config: ctConf}, }), }, } { diff --git a/identity/identity.go b/identity/identity.go index 55dd66155ea9..3f692b831f2b 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -507,6 +507,80 @@ func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Pro return &ii, nil } +func (i *Identity) deleteCredentialWebAuthFromIdentity() error { + cred, ok := i.GetCredentials(CredentialsTypeWebAuthn) + if !ok { + // This should never happend as it's checked earlier in the code; + // But we never know... + return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a WebAuthn credential but this user has no such credential set up.")) + } + + var cc CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + // Database has been tampered or the json schema are incompatible (migration issue); + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + updated := make([]CredentialWebAuthn, 0) + for k, cred := range cc.Credentials { + if cred.IsPasswordless { + updated = append(updated, cc.Credentials[k]) + } + } + + if len(updated) == 0 { + i.DeleteCredentialsType(CredentialsTypeWebAuthn) + return nil + } + + cc.Credentials = updated + message, err := json.Marshal(cc) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + + cred.Config = message + i.SetCredentials(CredentialsTypeWebAuthn, *cred) + return nil +} + +func (i *Identity) deleteCredentialOIDCFromIdentity(identifierToDelete string) error { + if identifierToDelete == "" { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You must provide an identifier to delete this credential.")) + } + _, hasOIDC := i.GetCredentials(CredentialsTypeOIDC) + if !hasOIDC { + return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove an OIDC credential but this user has no such credential set up.")) + } + var oidcConfig CredentialsOIDC + creds, err := i.ParseCredentials(CredentialsTypeOIDC, &oidcConfig) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + var updatedIdentifiers []string + var updatedProviders []CredentialsOIDCProvider + var found bool + for _, cfg := range oidcConfig.Providers { + if identifierToDelete == OIDCUniqueID(cfg.Provider, cfg.Subject) { + found = true + continue + } + updatedIdentifiers = append(updatedIdentifiers, OIDCUniqueID(cfg.Provider, cfg.Subject)) + updatedProviders = append(updatedProviders, cfg) + } + if !found { + return errors.WithStack(herodot.ErrNotFound.WithReasonf("The identifier `%s` was not found among OIDC credentials.", identifierToDelete)) + } + creds.Identifiers = updatedIdentifiers + creds.Config, err = json.Marshal(&CredentialsOIDC{Providers: updatedProviders}) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + i.Credentials[CredentialsTypeOIDC] = *creds + return nil +} + // Patch Identities Parameters // // swagger:parameters batchPatchIdentities diff --git a/identity/identity_test.go b/identity/identity_test.go index 726011fd00eb..d14388feacd4 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -10,21 +10,16 @@ import ( "fmt" "testing" - "github.com/ory/x/snapshotx" - - "github.com/ory/kratos/cipher" - "github.com/ory/kratos/x" - + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/gofrs/uuid" - - "github.com/ory/x/sqlxx" - + "github.com/ory/kratos/cipher" "github.com/ory/kratos/driver/config" - - "github.com/stretchr/testify/assert" + "github.com/ory/kratos/x" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" ) func TestNewIdentity(t *testing.T) { @@ -387,3 +382,58 @@ func TestWithDeclassifiedCredentials(t *testing.T) { } }) } + +func TestDeleteCredentialOIDCFromIdentity(t *testing.T) { + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + + err := i.deleteCredentialOIDCFromIdentity("") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("does-not-exist") + assert.Error(t, err) + + credentials := map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{"zab", "bar"}, + Type: CredentialsTypePassword, + Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + }, + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"bar:1234", "baz:5678"}, + Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "bar", "subject": "1234"}, {"provider": "baz", "subject": "5678"}]}`), + }, + CredentialsTypeWebAuthn: { + Type: CredentialsTypeWebAuthn, + Identifiers: []string{"foo", "bar"}, + Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + }, + } + i.Credentials = credentials + + err = i.deleteCredentialOIDCFromIdentity("zab") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("foo") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("bar") + assert.Error(t, err, "matches multiple OIDC credentials") + + require.NoError(t, i.deleteCredentialOIDCFromIdentity("bar:1234")) + + assert.Len(t, i.Credentials, 3) + + assert.Contains(t, i.Credentials, CredentialsTypePassword) + assert.EqualValues(t, i.Credentials[CredentialsTypePassword].Identifiers, []string{"zab", "bar"}) + + assert.Contains(t, i.Credentials, CredentialsTypeWebAuthn) + assert.EqualValues(t, i.Credentials[CredentialsTypeWebAuthn].Identifiers, []string{"foo", "bar"}) + + assert.Contains(t, i.Credentials, CredentialsTypeOIDC) + + oidc, ok := i.GetCredentials(CredentialsTypeOIDC) + require.True(t, ok) + assert.EqualValues(t, oidc.Identifiers, []string{"baz:5678"}) + var cfg CredentialsOIDC + _, err = i.ParseCredentials(CredentialsTypeOIDC, &cfg) + require.NoError(t, err) + assert.EqualValues(t, CredentialsOIDC{Providers: []CredentialsOIDCProvider{{Provider: "baz", Subject: "5678"}}}, cfg) +} From 7df3d561debb92de59b38a4e0f2c13d4f1e3f091 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:22:48 +0000 Subject: [PATCH 126/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/api_identity.go | 21 +++++++++++++++------ internal/httpclient/api_identity.go | 21 +++++++++++++++------ spec/api.json | 12 ++++++++++-- spec/swagger.json | 10 ++++++++-- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index b1201db57750..b48819525a13 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -110,11 +110,11 @@ type IdentityApi interface { /* * DeleteIdentityCredentials Delete a credential for a specific identity - * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type - You can only delete second factor (aal2) credentials. + * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. + You cannot delete password or code auth credentials through this API. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode * @return IdentityApiApiDeleteIdentityCredentialsRequest */ DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest @@ -1071,6 +1071,12 @@ type IdentityApiApiDeleteIdentityCredentialsRequest struct { ApiService IdentityApi id string type_ string + identifier *string +} + +func (r IdentityApiApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityApiApiDeleteIdentityCredentialsRequest { + r.identifier = &identifier + return r } func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { @@ -1079,12 +1085,12 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons /* - DeleteIdentityCredentials Delete a credential for a specific identity - - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type + - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. -You can only delete second factor (aal2) credentials. +You cannot delete password or code auth credentials through this API. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - @return IdentityApiApiDeleteIdentityCredentialsRequest */ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { @@ -1121,6 +1127,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.identifier != nil { + localVarQueryParams.Add("identifier", parameterToString(*r.identifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index b1201db57750..b48819525a13 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -110,11 +110,11 @@ type IdentityApi interface { /* * DeleteIdentityCredentials Delete a credential for a specific identity - * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type - You can only delete second factor (aal2) credentials. + * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. + You cannot delete password or code auth credentials through this API. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode * @return IdentityApiApiDeleteIdentityCredentialsRequest */ DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest @@ -1071,6 +1071,12 @@ type IdentityApiApiDeleteIdentityCredentialsRequest struct { ApiService IdentityApi id string type_ string + identifier *string +} + +func (r IdentityApiApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityApiApiDeleteIdentityCredentialsRequest { + r.identifier = &identifier + return r } func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { @@ -1079,12 +1085,12 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons /* - DeleteIdentityCredentials Delete a credential for a specific identity - - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type + - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. -You can only delete second factor (aal2) credentials. +You cannot delete password or code auth credentials through this API. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - @return IdentityApiApiDeleteIdentityCredentialsRequest */ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { @@ -1121,6 +1127,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.identifier != nil { + localVarQueryParams.Add("identifier", parameterToString(*r.identifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/spec/api.json b/spec/api.json index 582e44a779f1..f8a84f6f7d97 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4355,7 +4355,7 @@ }, "/admin/identities/{id}/credentials/{type}": { "delete": { - "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.", + "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.", "operationId": "deleteIdentityCredentials", "parameters": [ { @@ -4368,7 +4368,7 @@ } }, { - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "in": "path", "name": "type", "required": true, @@ -4388,6 +4388,14 @@ "type": "string" }, "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + }, + { + "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.", + "in": "query", + "name": "identifier", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index e77bfefa6307..570cb4003d62 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -662,7 +662,7 @@ "oryAccessToken": [] } ], - "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.", + "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.", "consumes": [ "application/json" ], @@ -701,10 +701,16 @@ ], "type": "string", "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "name": "type", "in": "path", "required": true + }, + { + "type": "string", + "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.", + "name": "identifier", + "in": "query" } ], "responses": { From c5089801af2a656e9c1fc371a11aeb23918ba359 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Mon, 1 Jul 2024 12:31:14 +0200 Subject: [PATCH 127/262] docs: typo in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a081e3be7f..58628071c945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -329,7 +329,7 @@ This feature enables two-step registration per default. Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To disable two-step registration, set -`selfservice.flows.registration.enable_legacy_flow` to `true`. This value +`selfservice.flows.registration.enable_legacy_one_step` to `true`. This value defaults to `false`. ### Bug Fixes From 7c5299f1f832ebbe0622d0920b7a91253d26b06c Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 2 Jul 2024 10:23:22 +0200 Subject: [PATCH 128/262] fix: jsonnet timeouts (#3979) --- corpx/faker.go | 6 +- go.mod | 38 ++++++------- go.sum | 85 +++++++++++++++------------- persistence/sql/persister_session.go | 5 +- session/session.go | 8 +-- 5 files changed, 74 insertions(+), 68 deletions(-) diff --git a/corpx/faker.go b/corpx/faker.go index e8fc4b0e388f..ec54a252ab6b 100644 --- a/corpx/faker.go +++ b/corpx/faker.go @@ -17,8 +17,8 @@ import ( "github.com/ory/kratos/session" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/pointerx" "github.com/ory/x/randx" - "github.com/ory/x/stringsx" ) var setup sync.Once @@ -31,13 +31,13 @@ func registerFakes() { _ = faker.SetRandomMapAndSliceSize(4) if err := faker.AddProvider("ptr_geo_location", func(v reflect.Value) (interface{}, error) { - return stringsx.GetPointer("Munich, Germany"), nil + return pointerx.Ptr("Munich, Germany"), nil }); err != nil { panic(err) } if err := faker.AddProvider("ptr_ipv4", func(v reflect.Value) (interface{}, error) { - return stringsx.GetPointer(faker.IPv4()), nil + return pointerx.Ptr(faker.IPv4()), nil }); err != nil { panic(err) } diff --git a/go.mod b/go.mod index 67e7a524c134..47238d202c79 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 github.com/dgraph-io/ristretto v0.1.1 - github.com/fatih/color v1.13.0 + github.com/fatih/color v1.16.0 github.com/ghodss/yaml v1.0.0 github.com/go-crypt/crypt v0.2.9 github.com/go-faker/faker/v4 v4.2.0 @@ -50,7 +50,7 @@ require ( github.com/gorilla/sessions v1.2.1 github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 github.com/hashicorp/consul/api v1.20.0 - github.com/hashicorp/go-retryablehttp v0.7.2 + github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/golang-lru v0.5.4 github.com/imdario/mergo v0.3.13 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf @@ -69,7 +69,7 @@ require ( github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe github.com/ory/analytics-go/v5 v5.0.1 github.com/ory/client-go v0.2.0-alpha.60 - github.com/ory/dockertest/v3 v3.9.1 + github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8 github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88 @@ -77,7 +77,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.623 + github.com/ory/x v0.0.639 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 @@ -85,7 +85,7 @@ require ( github.com/rakutentech/jwk-go v1.1.3 github.com/rs/cors v1.8.2 github.com/samber/lo v1.37.0 - github.com/sirupsen/logrus v1.9.0 + github.com/sirupsen/logrus v1.9.3 github.com/slack-go/slack v0.7.4 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 @@ -110,11 +110,11 @@ require ( ) require ( - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect - github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/a8m/envsubst v1.3.0 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect @@ -126,17 +126,17 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cockroachdb/cockroach-go/v2 v2.3.5 - github.com/containerd/continuity v0.3.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e // indirect github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/docker/cli v20.10.21+incompatible // indirect + github.com/docker/cli v24.0.9+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v20.10.27+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/elliotchance/orderedmap v1.4.0 // indirect @@ -162,7 +162,7 @@ require ( github.com/go-openapi/validate v0.22.1 // indirect github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-webauthn/x v0.1.4 // indirect github.com/gobuffalo/envy v1.10.2 // indirect github.com/gobuffalo/flect v1.0.0 // indirect @@ -195,7 +195,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -233,7 +233,7 @@ require ( github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/lib/pq v1.10.7 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailhog/MailHog-Server v1.0.1 // indirect github.com/mailhog/MailHog-UI v1.0.1 // indirect @@ -244,21 +244,21 @@ require ( github.com/mailhog/storage v1.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect + github.com/moby/term v0.5.0 // indirect github.com/nyaruka/phonenumbers v1.3.6 // indirect github.com/ogier/pflag v0.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect - github.com/opencontainers/runc v1.1.12 // indirect + github.com/opencontainers/runc v1.1.13 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect @@ -308,7 +308,7 @@ require ( go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/tools v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect diff --git a/go.sum b/go.sum index 85e85e83626c..33830713d3b9 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f code.dny.dev/ssrf v0.2.0 h1:wCBP990rQQ1CYfRpW+YK1+8xhwUjv189AQ3WMo1jQaI= code.dny.dev/ssrf v0.2.0/go.mod h1:B+91l25OnyaLIeCx0WRJN5qfJ/4/ZTZxRXgm0lj/2w8= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -52,8 +52,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -105,8 +105,8 @@ github.com/bwmarrin/discordgo v0.23.0 h1://ARp8qUrRZvDGMkfAjtcC20WOvsMtTgi+KrdKn github.com/bwmarrin/discordgo v0.23.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -125,8 +125,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0= github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= -github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= -github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -140,8 +140,9 @@ github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec/go.mod h1:10Fm2kas github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -156,14 +157,14 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU= -github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50= +github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.27+incompatible h1:Id/ZooynV4ZlD6xX20RCd3SR0Ikn7r4QZDa2ECK2TgA= github.com/docker/docker v20.10.27+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= @@ -181,8 +182,9 @@ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2Vvl github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -480,9 +482,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -493,8 +494,8 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= @@ -689,8 +690,9 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA= github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -728,18 +730,19 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.7 h1:vzy0i4a2iDzEFMdXIxcanRadkr0FBvSBKUmj0P8SPlQ= @@ -748,8 +751,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= -github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= -github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -769,8 +772,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= -github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -802,14 +805,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM= github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= -github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= -github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= +github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8 h1:pdmvNMAN5x5kPmntdHNmfl3TDszlGeXYri+JSA4JMNM= +github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8/go.mod h1:Z3wDt3X5YzB70upzvwiBH2U3lj8q/SXHKT2dyMM7t3I= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 h1:VMUeLRfQD14fOMvhpYZIIT4vtAqxYh+f3KnSqCeJ13o= @@ -827,8 +830,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.623 h1:sFJiw2i/itTkBRJbhGXtrso9NcdscnjFlHBFitCzf8A= -github.com/ory/x v0.0.623/go.mod h1:CUw8/O3X8lUMheyV0iH+6LQ0tePrH+FBsW39MccCHgw= +github.com/ory/x v0.0.639 h1:6/9V6XlAwsPBFNpL/FMp83SbFD70n0Ql0dAaAlDbESA= +github.com/ory/x v0.0.639/go.mod h1:kjXXSK3a0lC9NNSkxG1sRlnrR9GoG52mvo8z4Nsicu0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -935,8 +938,9 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.7.4 h1:Z+7CmUDV+ym4lYLA4NNLFIpr3+nDgViHrx8xsuXgrYs= github.com/slack-go/slack v0.7.4/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -980,6 +984,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1270,7 +1275,6 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1314,10 +1318,12 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1328,11 +1334,12 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/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.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1386,7 +1393,6 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1593,9 +1599,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= -gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go index 412ed2d8a825..37fccca0bd84 100644 --- a/persistence/sql/persister_session.go +++ b/persistence/sql/persister_session.go @@ -10,6 +10,7 @@ import ( "github.com/ory/herodot" "github.com/ory/x/dbal" + "github.com/ory/x/pointerx" "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" @@ -286,10 +287,10 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err device.NID = s.NID if device.Location != nil { - device.Location = stringsx.GetPointer(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength)) + device.Location = pointerx.Ptr(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength)) } if device.UserAgent != nil { - device.UserAgent = stringsx.GetPointer(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength)) + device.UserAgent = pointerx.Ptr(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength)) } if err := p.DevicePersister.CreateDevice(ctx, device); err != nil { diff --git a/session/session.go b/session/session.go index 84f64ceec0d6..e5b826b88f2f 100644 --- a/session/session.go +++ b/session/session.go @@ -16,7 +16,7 @@ import ( "github.com/ory/x/httpx" "github.com/ory/x/pagination/keysetpagination" - "github.com/ory/x/stringsx" + "github.com/ory/x/pointerx" "github.com/pkg/errors" @@ -282,12 +282,12 @@ func (s *Session) Activate(r *http.Request, i *identity.Identity, c lifespanProv func (s *Session) SetSessionDeviceInformation(r *http.Request) { device := Device{ SessionID: s.ID, - IPAddress: stringsx.GetPointer(httpx.ClientIP(r)), + IPAddress: pointerx.Ptr(httpx.ClientIP(r)), } agent := r.Header["User-Agent"] if len(agent) > 0 { - device.UserAgent = stringsx.GetPointer(strings.Join(agent, " ")) + device.UserAgent = pointerx.Ptr(strings.Join(agent, " ")) } var clientGeoLocation []string @@ -297,7 +297,7 @@ func (s *Session) SetSessionDeviceInformation(r *http.Request) { if r.Header.Get("Cf-Ipcountry") != "" { clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcountry")) } - device.Location = stringsx.GetPointer(strings.Join(clientGeoLocation, ", ")) + device.Location = pointerx.Ptr(strings.Join(clientGeoLocation, ", ")) s.Devices = append(s.Devices, device) } From c9d55730a10b71ac61bb5097f5f9c33f144f2a95 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Wed, 3 Jul 2024 09:27:58 +0200 Subject: [PATCH 129/262] feat: password migration hook (#3978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a password migration hook to easily migrate passwords for which we do not have the hash. For each user that needs to be migrated to Ory Network, a new identity is created with a credential of type password with a config of {"use_password_migration_hook": true} . When a user logs in, the credential identifier and password will be sent to the password_migration web hook if all of these are true: The user’s identity’s password credential is {"use_password_migration_hook": true} The password_migration hook is configured After calling the password_migration hook, the HTTP status code will be inspected: On 200, we parse the response as JSON and look for {"status": "password_match"}. The password credential config will be replaced with the hash of the actual password. On any other status code, we assume that the password is not valid. --------- Co-authored-by: zepatrik --- .github/workflows/ci.yaml | 5 +- .golangci.yml | 8 +- Makefile | 2 +- driver/config/config.go | 19 +- embedx/config.schema.json | 647 ++++++-------------- identity/credentials_password.go | 9 + identity/credentials_password_test.go | 46 ++ internal/client-go/go.sum | 1 + selfservice/hook/password_migration_hook.go | 111 ++++ selfservice/strategy/password/login.go | 32 +- selfservice/strategy/password/login_test.go | 229 ++++++- selfservice/strategy/password/strategy.go | 10 +- 12 files changed, 620 insertions(+), 499 deletions(-) create mode 100644 identity/credentials_password_test.go create mode 100644 selfservice/hook/password_migration_hook.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 26df4ffc97a3..9c2ee0555b42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -88,13 +88,12 @@ jobs: - run: npm install name: Install node deps - name: Run golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 env: GOGC: 100 with: args: --timeout 10m0s - version: v1.56.2 - skip-pkg-cache: true + version: v1.59.1 - name: Build Kratos run: make install - name: Run go-acc (tests) diff --git a/.golangci.yml b/.golangci.yml index 374c9204ed1b..e83dd5a56a2e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,14 +19,12 @@ linters-settings: goimports: local-prefixes: github.com/ory -run: - skip-dirs: +issues: + exclude-dirs: - sdk/ - skip-files: + exclude-files: - ".+_test.go" - "corpx/faker.go" - -issues: exclude: - "Set is deprecated: use context-based WithConfigValue instead" - "SetDefaultIdentitySchemaFromRaw is deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead" diff --git a/Makefile b/Makefile index 61e4284d3994..7af282d469c3 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ docs/swagger: npx @redocly/openapi-cli preview-docs spec/swagger.json .bin/golangci-lint: Makefile - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.56.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.59.1 .bin/hydra: Makefile bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v2.2.0-rc.3 diff --git a/driver/config/config.go b/driver/config/config.go index 9f3c1b38938b..ac394d7c8518 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -203,6 +203,7 @@ const ( ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls" ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level" ViperKeyVersion = "version" + ViperKeyPasswordMigrationHook = "selfservice.flows.login.password_migration" ) const ( @@ -290,6 +291,10 @@ type ( Headers map[string]string `json:"headers" koanf:"headers"` LocalName string `json:"local_name" koanf:"local_name"` } + PasswordMigrationHook struct { + Enabled bool `json:"enabled"` + Config json.RawMessage `json:"config"` + } Config struct { l *logrusx.Logger p *configx.Provider @@ -518,13 +523,13 @@ func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) { }) } -// Deprecatd: use context-based WithConfigValue instead -func (p *Config) Set(ctx context.Context, key string, value interface{}) error { +// Deprecated: use context-based WithConfigValue instead +func (p *Config) Set(_ context.Context, key string, value interface{}) error { return p.p.Set(key, value) } // Deprecated: use context-based WithConfigValue instead -func (p *Config) MustSet(ctx context.Context, key string, value interface{}) { +func (p *Config) MustSet(_ context.Context, key string, value interface{}) { if err := p.p.Set(key, value); err != nil { p.l.WithError(err).Fatalf("Unable to set \"%s\" to \"%s\".", key, value) } @@ -1599,3 +1604,11 @@ func (p *Config) TokenizeTemplate(ctx context.Context, key string) (_ *SessionTo func (p *Config) DefaultConsistencyLevel(ctx context.Context) crdbx.ConsistencyLevel { return crdbx.ConsistencyLevelFromString(p.GetProvider(ctx).String(ViperKeyPreviewDefaultReadConsistencyLevel)) } + +func (p *Config) PasswordMigrationHook(ctx context.Context) (hook *PasswordMigrationHook) { + hook = new(PasswordMigrationHook) + // Error is ignored on purpose, as we then default to a hook with `enabled = false`. + _ = p.GetProvider(ctx).Unmarshal(ViperKeyPasswordMigrationHook, hook) + + return hook +} diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 5ebbdb1241ee..e763c402a91a 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -43,10 +43,7 @@ "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/dashboard", - "/dashboard" - ] + "examples": ["https://my-app.com/dashboard", "/dashboard"] }, "selfServiceSessionRevokerHook": { "type": "object", @@ -56,9 +53,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceSessionIssuerHook": { "type": "object", @@ -68,9 +63,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceRequireVerifiedAddressHook": { "type": "object", @@ -80,9 +73,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceVerificationHook": { "type": "object", @@ -92,9 +83,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceShowVerificationUIHook": { "type": "object", @@ -104,9 +93,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "b2bSSOHook": { "type": "object", @@ -120,10 +107,7 @@ } }, "additionalProperties": false, - "required": [ - "hook", - "config" - ] + "required": ["hook", "config"] }, "webHookAuthBasicAuthProperties": { "properties": { @@ -143,17 +127,11 @@ } }, "additionalProperties": false, - "required": [ - "user", - "password" - ] + "required": ["user", "password"] } }, "additionalProperties": false, - "required": [ - "type", - "config" - ] + "required": ["type", "config"] }, "httpRequestConfig": { "type": "object", @@ -161,9 +139,7 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to send the emails to.", - "examples": [ - "https://example.com/api/v1/email" - ], + "examples": ["https://example.com/api/v1/email"], "type": "string", "pattern": "^https?://" }, @@ -228,25 +204,15 @@ "in": { "type": "string", "description": "How the api key should be transferred", - "enum": [ - "header", - "cookie" - ] + "enum": ["header", "cookie"] } }, "additionalProperties": false, - "required": [ - "name", - "value", - "in" - ] + "required": ["name", "value", "in"] } }, "additionalProperties": false, - "required": [ - "type", - "config" - ] + "required": ["type", "config"] }, "selfServiceWebHook": { "type": "object", @@ -285,10 +251,7 @@ "const": true } }, - "required": [ - "ignore", - "parse" - ] + "required": ["ignore", "parse"] } }, "url": { @@ -361,46 +324,30 @@ "response": { "properties": { "ignore": { - "enum": [ - true - ] + "enum": [true] } }, - "required": [ - "ignore" - ] + "required": ["ignore"] } }, - "required": [ - "response" - ] + "required": ["response"] } }, { "properties": { "can_interrupt": { - "enum": [ - false - ] + "enum": [false] } }, - "require": [ - "can_interrupt" - ] + "require": ["can_interrupt"] } ], "additionalProperties": false, - "required": [ - "url", - "method" - ] + "required": ["url", "method"] } }, "additionalProperties": false, - "required": [ - "hook", - "config" - ] + "required": ["hook", "config"] }, "OIDCClaims": { "title": "OpenID Connect claims", @@ -433,9 +380,7 @@ "essential": true }, "acr": { - "values": [ - "urn:mace:incommon:iap:silver" - ] + "values": ["urn:mace:incommon:iap:silver"] } } } @@ -483,9 +428,7 @@ "properties": { "id": { "type": "string", - "examples": [ - "google" - ] + "examples": ["google"] }, "provider": { "title": "Provider", @@ -514,9 +457,7 @@ "lark", "x" ], - "examples": [ - "google" - ] + "examples": ["google"] }, "label": { "title": "Optional string which will be used when generating labels for UI buttons.", @@ -531,23 +472,17 @@ "issuer_url": { "type": "string", "format": "uri", - "examples": [ - "https://accounts.google.com" - ] + "examples": ["https://accounts.google.com"] }, "auth_url": { "type": "string", "format": "uri", - "examples": [ - "https://accounts.google.com/o/oauth2/v2/auth" - ] + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] }, "token_url": { "type": "string", "format": "uri", - "examples": [ - "https://www.googleapis.com/oauth2/v4/token" - ] + "examples": ["https://www.googleapis.com/oauth2/v4/token"] }, "mapper_url": { "title": "Jsonnet Mapper URL", @@ -564,10 +499,7 @@ "type": "array", "items": { "type": "string", - "examples": [ - "offline_access", - "profile" - ] + "examples": ["offline_access", "profile"] } }, "microsoft_tenant": { @@ -586,30 +518,21 @@ "title": "Microsoft subject source", "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier.", "type": "string", - "enum": [ - "userinfo", - "me" - ], + "enum": ["userinfo", "me"], "default": "userinfo", - "examples": [ - "userinfo" - ] + "examples": ["userinfo"] }, "apple_team_id": { "title": "Apple Developer Team ID", "description": "Apple Developer Team ID needed for generating a JWT token for client secret", "type": "string", - "examples": [ - "KP76DQS54M" - ] + "examples": ["KP76DQS54M"] }, "apple_private_key_id": { "title": "Apple Private Key Identifier", "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", "type": "string", - "examples": [ - "UX56C66723" - ] + "examples": ["UX56C66723"] }, "apple_private_key": { "title": "Apple Private Key", @@ -626,42 +549,27 @@ "title": "Organization ID", "description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.", "type": "string", - "examples": [ - "12345678-1234-1234-1234-123456789012" - ] + "examples": ["12345678-1234-1234-1234-123456789012"] }, "additional_id_token_audiences": { "title": "Additional client ids allowed when using ID token submission", "type": "array", "items": { "type": "string", - "examples": [ - "12345678-1234-1234-1234-123456789012" - ] + "examples": ["12345678-1234-1234-1234-123456789012"] } }, "claims_source": { "title": "Claims source", "description": "Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`", "type": "string", - "enum": [ - "id_token", - "userinfo" - ], + "enum": ["id_token", "userinfo"], "default": "id_token", - "examples": [ - "id_token", - "userinfo" - ] + "examples": ["id_token", "userinfo"] } }, "additionalProperties": false, - "required": [ - "id", - "provider", - "client_id", - "mapper_url" - ], + "required": ["id", "provider", "client_id", "mapper_url"], "allOf": [ { "if": { @@ -670,23 +578,17 @@ "const": "microsoft" } }, - "required": [ - "provider" - ] + "required": ["provider"] }, "then": { - "required": [ - "microsoft_tenant" - ] + "required": ["microsoft_tenant"] }, "else": { "not": { "properties": { "microsoft_tenant": {} }, - "required": [ - "microsoft_tenant" - ] + "required": ["microsoft_tenant"] } } }, @@ -697,9 +599,7 @@ "const": "apple" } }, - "required": [ - "provider" - ] + "required": ["provider"] }, "then": { "not": { @@ -709,9 +609,7 @@ "minLength": 1 } }, - "required": [ - "client_secret" - ] + "required": ["client_secret"] }, "required": [ "apple_private_key_id", @@ -720,9 +618,7 @@ ] }, "else": { - "required": [ - "client_secret" - ], + "required": ["client_secret"], "allOf": [ { "not": { @@ -732,9 +628,7 @@ "minLength": 1 } }, - "required": [ - "apple_team_id" - ] + "required": ["apple_team_id"] } }, { @@ -745,9 +639,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key_id" - ] + "required": ["apple_private_key_id"] } }, { @@ -758,9 +650,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key" - ] + "required": ["apple_private_key"] } } ] @@ -940,10 +830,7 @@ "title": "Required Authenticator Assurance Level", "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.", "type": "string", - "enum": [ - "aal1", - "highest_available" - ], + "enum": ["aal1", "highest_available"], "default": "highest_available" }, "selfServiceAfterSettings": { @@ -1139,9 +1026,7 @@ "path": { "title": "Path to PEM-encoded Fle", "type": "string", - "examples": [ - "path/to/file.pem" - ] + "examples": ["path/to/file.pem"] }, "base64": { "title": "Base64 Encoded Inline", @@ -1189,9 +1074,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] }, "valid": { "additionalProperties": false, @@ -1204,9 +1087,7 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } }, @@ -1277,9 +1158,7 @@ "selfservice": { "type": "object", "additionalProperties": false, - "required": [ - "default_browser_return_url" - ], + "required": ["default_browser_return_url"], "properties": { "default_browser_return_url": { "$ref": "#/definitions/defaultReturnTo" @@ -1314,30 +1193,20 @@ "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/user/settings" - ], + "examples": ["https://my-app.com/user/settings"], "default": "https://www.ory.sh/kratos/docs/fallback/settings" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "privileged_session_max_age": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "required_aal": { "$ref": "#/definitions/featureRequiredAal" @@ -1386,20 +1255,14 @@ "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/signup" - ], + "examples": ["https://my-app.com/signup"], "default": "https://www.ory.sh/kratos/docs/fallback/registration" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeRegistration" @@ -1424,31 +1287,77 @@ "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/login" - ], + "examples": ["https://my-app.com/login"], "default": "https://www.ory.sh/kratos/docs/fallback/login" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "style": { "title": "Login Flow Style", "description": "The style of the login flow. If set to `one_step` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.", "type": "string", - "enum": [ - "one_step", - "identifier_first" - ], + "enum": ["one_step", "identifier_first"], "default": "one_step" }, + "password_migration": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Password Migration", + "description": "If set to true will enable password migration.", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The URL the password migration hook should call", + "format": "uri" + }, + "method": { + "type": "string", + "description": "The HTTP method to use (GET, POST, etc).", + "const": "POST", + "default": "POST" + }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the password migration hook.", + "additionalProperties": { + "type": "string" + } + }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this hook on delivery or error" + }, + "auth": { + "type": "object", + "title": "Auth mechanisms", + "description": "Define which auth mechanism the Web-Hook should use", + "oneOf": [ + { + "$ref": "#/definitions/webHookAuthApiKeyProperties" + }, + { + "$ref": "#/definitions/webHookAuthBasicAuthProperties" + } + ] + }, + "additionalProperties": false + } + } + } + }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" }, @@ -1473,9 +1382,7 @@ "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/verify" - ], + "examples": ["https://my-app.com/verify"], "default": "https://www.ory.sh/kratos/docs/fallback/verification" }, "after": { @@ -1487,11 +1394,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeVerification" @@ -1500,10 +1403,7 @@ "title": "Verification Strategy", "description": "The strategy to use for verification requests", "type": "string", - "enum": [ - "link", - "code" - ], + "enum": ["link", "code"], "default": "code" }, "notify_unknown_recipients": { @@ -1530,9 +1430,7 @@ "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/verify" - ], + "examples": ["https://my-app.com/verify"], "default": "https://www.ory.sh/kratos/docs/fallback/recovery" }, "after": { @@ -1544,11 +1442,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeRecovery" @@ -1557,10 +1451,7 @@ "title": "Recovery Strategy", "description": "The strategy to use for recovery requests", "type": "string", - "enum": [ - "link", - "code" - ], + "enum": ["link", "code"], "default": "code" }, "notify_unknown_recipients": { @@ -1580,9 +1471,7 @@ "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/kratos-error" - ], + "examples": ["https://my-app.com/kratos-error"], "default": "https://www.ory.sh/kratos/docs/fallback/error" } } @@ -1611,25 +1500,19 @@ "type": "string", "description": "The ID of the organization.", "format": "uuid", - "examples": [ - "00000000-0000-0000-0000-000000000000" - ] + "examples": ["00000000-0000-0000-0000-000000000000"] }, "label": { "type": "string", "description": "The label of the organization.", - "examples": [ - "ACME SSO" - ] + "examples": ["ACME SSO"] }, "domains": { "type": "array", "items": { "type": "string", "format": "hostname", - "examples": [ - "my-app.com" - ], + "examples": ["my-app.com"], "description": "If this domain matches the email's domain, this provider is shown." } } @@ -1669,20 +1552,14 @@ "base_url": { "title": "Override the base URL which should be used as the base for recovery and verification links.", "type": "string", - "examples": [ - "https://my-app.com" - ] + "examples": ["https://my-app.com"] }, "lifespan": { "title": "How long a link is valid for", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } } @@ -1756,11 +1633,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } } @@ -1883,17 +1756,13 @@ "type": "string", "title": "Relying Party Display Name", "description": "An name to help the user identify this RP.", - "examples": [ - "Ory Foundation" - ] + "examples": ["Ory Foundation"] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": [ - "ory.sh" - ] + "examples": ["ory.sh"] }, "origin": { "type": "string", @@ -1901,9 +1770,7 @@ "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).", "format": "uri", "deprecationMessage": "This field is deprecated. Use `origins` instead.", - "examples": [ - "https://www.ory.sh" - ] + "examples": ["https://www.ory.sh"] }, "origins": { "type": "array", @@ -1924,18 +1791,13 @@ "description": "An icon to help the user identify this RP.", "format": "uri", "deprecationMessage": "This field is deprecated and ignored due to security considerations.", - "examples": [ - "https://www.ory.sh/an-icon.png" - ] + "examples": ["https://www.ory.sh/an-icon.png"] } }, "type": "object", "oneOf": [ { - "required": [ - "id", - "display_name" - ], + "required": ["id", "display_name"], "properties": { "origin": { "not": {} @@ -1946,11 +1808,7 @@ } }, { - "required": [ - "id", - "display_name", - "origin" - ], + "required": ["id", "display_name", "origin"], "properties": { "origin": { "type": "string" @@ -1961,11 +1819,7 @@ } }, { - "required": [ - "id", - "display_name", - "origins" - ], + "required": ["id", "display_name", "origins"], "properties": { "origin": { "not": {} @@ -1990,14 +1844,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] }, "then": { - "required": [ - "config" - ] + "required": ["config"] } }, "passkey": { @@ -2020,17 +1870,13 @@ "type": "string", "title": "Relying Party Display Name", "description": "A name to help the user identify this RP.", - "examples": [ - "Ory Foundation" - ] + "examples": ["Ory Foundation"] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": [ - "ory.sh" - ] + "examples": ["ory.sh"] }, "origins": { "type": "array", @@ -2047,10 +1893,7 @@ } }, "type": "object", - "required": [ - "display_name", - "id" - ] + "required": ["display_name", "id"] } }, "additionalProperties": false @@ -2062,14 +1905,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] }, "then": { - "required": [ - "config" - ] + "required": ["config"] } }, "oidc": { @@ -2092,9 +1931,7 @@ "title": "Base URL for OAuth2 Redirect URIs", "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.", "format": "uri", - "examples": [ - "https://auth.myexample.org/" - ] + "examples": ["https://auth.myexample.org/"] }, "providers": { "title": "OpenID Connect and OAuth2 Providers", @@ -2199,9 +2036,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } }, @@ -2220,9 +2055,7 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } } @@ -2232,18 +2065,13 @@ "type": "string", "title": "Override message templates", "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", - "examples": [ - "/conf/courier-templates" - ] + "examples": ["/conf/courier-templates"] }, "message_retries": { "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned", "type": "integer", "default": 5, - "examples": [ - 10, - 60 - ] + "examples": [10, 60] }, "worker": { "description": "Configures the dispatch worker.", @@ -2266,10 +2094,7 @@ "title": "Delivery Strategy", "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", "type": "string", - "enum": [ - "smtp", - "http" - ], + "enum": ["smtp", "http"], "default": "smtp" }, "http": { @@ -2326,9 +2151,7 @@ "title": "SMTP Sender Name", "description": "The recipient of an email will see this as the sender name.", "type": "string", - "examples": [ - "Bob" - ] + "examples": ["Bob"] }, "headers": { "title": "SMTP Headers", @@ -2376,9 +2199,7 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to connect to the SMS provider.", - "examples": [ - "https://api.twillio.com/sms/send" - ], + "examples": ["https://api.twillio.com/sms/send"], "type": "string", "pattern": "^https?:\\/\\/.*" }, @@ -2420,10 +2241,7 @@ }, "additionalProperties": false }, - "required": [ - "url", - "method" - ], + "required": ["url", "method"], "additionalProperties": false } }, @@ -2440,26 +2258,19 @@ "title": "Channel id", "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only phone is supported.", "maxLength": 32, - "enum": [ - "sms" - ] + "enum": ["sms"] }, "type": { "type": "string", "title": "Channel type", "description": "The channel type. Currently only http is supported.", - "enum": [ - "http" - ] + "enum": ["http"] }, "request_config": { "$ref": "#/definitions/httpRequestConfig" } }, - "required": [ - "id", - "request_config" - ], + "required": ["id", "request_config"], "additionalProperties": false } } @@ -2510,10 +2321,7 @@ "type": "string", "title": "Default Read Consistency Level", "description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.", - "enum": [ - "strong", - "eventual" - ], + "enum": ["strong", "eventual"], "default": "strong" } } @@ -2541,9 +2349,7 @@ "description": "The URL where the admin endpoint is exposed at.", "type": "string", "format": "uri", - "examples": [ - "https://kratos.private-network:4434/" - ] + "examples": ["https://kratos.private-network:4434/"] }, "host": { "title": "Admin Host", @@ -2557,9 +2363,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 4434 }, "socket": { @@ -2618,9 +2422,7 @@ ] }, "uniqueItems": true, - "default": [ - "*" - ], + "default": ["*"], "examples": [ [ "https://example.com", @@ -2632,13 +2434,7 @@ "allowed_methods": { "type": "array", "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", - "default": [ - "POST", - "GET", - "PUT", - "PATCH", - "DELETE" - ], + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], "items": { "type": "string", "enum": [ @@ -2672,9 +2468,7 @@ "exposed_headers": { "type": "array", "description": "Sets which headers are safe to expose to the API of a CORS API specification.", - "default": [ - "Content-Type" - ], + "default": ["Content-Type"], "items": { "type": "string" } @@ -2717,9 +2511,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4433 - ], + "examples": [4433], "default": 4433 }, "socket": { @@ -2769,10 +2561,7 @@ "format": { "description": "The log format can either be text or JSON.", "type": "string", - "enum": [ - "json", - "text" - ] + "enum": ["json", "text"] } }, "additionalProperties": false @@ -2813,9 +2602,7 @@ "id": { "title": "The schema's ID.", "type": "string", - "examples": [ - "employee" - ] + "examples": ["employee"] }, "url": { "type": "string", @@ -2829,16 +2616,11 @@ ] } }, - "required": [ - "id", - "url" - ] + "required": ["id", "url"] } } }, - "required": [ - "schemas" - ], + "required": ["schemas"], "additionalProperties": false }, "secrets": { @@ -2887,10 +2669,7 @@ "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.", "type": "string", "default": "bcrypt", - "enum": [ - "argon2", - "bcrypt" - ] + "enum": ["argon2", "bcrypt"] }, "argon2": { "title": "Configuration for the Argon2id hasher.", @@ -2946,9 +2725,7 @@ "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.", "type": "object", "additionalProperties": false, - "required": [ - "cost" - ], + "required": ["cost"], "properties": { "cost": { "type": "integer", @@ -2970,11 +2747,7 @@ "description": "One of the values: noop, aes, xchacha20-poly1305", "type": "string", "default": "noop", - "enum": [ - "noop", - "aes", - "xchacha20-poly1305" - ] + "enum": ["noop", "aes", "xchacha20-poly1305"] } } }, @@ -2998,11 +2771,7 @@ "title": "HTTP Cookie Same Site Configuration", "description": "Sets the session and CSRF cookie SameSite.", "type": "string", - "enum": [ - "Strict", - "Lax", - "None" - ], + "enum": ["Strict", "Lax", "None"], "default": "Lax" } }, @@ -3032,9 +2801,7 @@ "patternProperties": { "[a-zA-Z0-9-_.]+": { "type": "object", - "required": [ - "jwks_url" - ], + "required": ["jwks_url"], "properties": { "ttl": { "type": "string", @@ -3067,11 +2834,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "cookie": { "type": "object", @@ -3102,11 +2865,7 @@ "title": "Session Cookie SameSite Configuration", "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.", "type": "string", - "enum": [ - "Strict", - "Lax", - "None" - ] + "enum": ["Strict", "Lax", "None"] } }, "additionalProperties": false @@ -3116,11 +2875,7 @@ "description": "Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value.", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } }, @@ -3129,9 +2884,7 @@ "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", "type": "string", "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$", - "examples": [ - "v0.5.0-alpha.1" - ] + "examples": ["v0.5.0-alpha.1"] }, "dev": { "type": "boolean" @@ -3155,9 +2908,7 @@ "type": "integer", "minimum": 0, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 0 }, "config": { @@ -3263,14 +3014,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "verification" - ] + "required": ["verification"] }, { "properties": { @@ -3280,31 +3027,21 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "recovery" - ] + "required": ["recovery"] } ] } }, - "required": [ - "flows" - ] + "required": ["flows"] } }, - "required": [ - "selfservice" - ] + "required": ["selfservice"] }, "then": { - "required": [ - "courier" - ] + "required": ["courier"] } }, { @@ -3323,33 +3060,21 @@ ] } }, - "required": [ - "algorithm" - ] + "required": ["algorithm"] } }, - "required": [ - "ciphers" - ] + "required": ["ciphers"] }, "then": { - "required": [ - "secrets" - ], + "required": ["secrets"], "properties": { "secrets": { - "required": [ - "cipher" - ] + "required": ["cipher"] } } } } ], - "required": [ - "identity", - "dsn", - "selfservice" - ], + "required": ["identity", "dsn", "selfservice"], "additionalProperties": false } diff --git a/identity/credentials_password.go b/identity/credentials_password.go index 85f5ac1d7d0c..4a6e7ebc7144 100644 --- a/identity/credentials_password.go +++ b/identity/credentials_password.go @@ -9,4 +9,13 @@ package identity type CredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword string `json:"hashed_password"` + + // UsePasswordMigrationHook is set to true if the password should be migrated + // using the password migration hook. If set, and the HashedPassword is empty, a + // webhook will be called during login to migrate the password. + UsePasswordMigrationHook bool `json:"use_password_migration_hook,omitempty"` +} + +func (cp *CredentialsPassword) ShouldUsePasswordMigrationHook() bool { + return cp != nil && cp.HashedPassword == "" && cp.UsePasswordMigrationHook } diff --git a/identity/credentials_password_test.go b/identity/credentials_password_test.go new file mode 100644 index 000000000000..6e62720779e6 --- /dev/null +++ b/identity/credentials_password_test.go @@ -0,0 +1,46 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCredentialsPassword_ShouldUsePasswordMigrationHook(t *testing.T) { + tests := []struct { + name string + cp *CredentialsPassword + want bool + }{{ + name: "pw set", + cp: &CredentialsPassword{ + HashedPassword: "pw", + UsePasswordMigrationHook: true, + }, + want: false, + }, { + name: "pw not set", + cp: &CredentialsPassword{ + HashedPassword: "", + UsePasswordMigrationHook: true, + }, + want: true, + }, { + name: "nil", + want: false, + }, { + name: "pw not set, hook not set", + cp: &CredentialsPassword{ + HashedPassword: "", + }, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, tt.cp.ShouldUsePasswordMigrationHook(), "ShouldUsePasswordMigrationHook()") + }) + } +} diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/hook/password_migration_hook.go b/selfservice/hook/password_migration_hook.go new file mode 100644 index 000000000000..065dc5dcddc6 --- /dev/null +++ b/selfservice/hook/password_migration_hook.go @@ -0,0 +1,111 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.11.0" + "go.opentelemetry.io/otel/trace" + grpccodes "google.golang.org/grpc/codes" + + "github.com/ory/herodot" + "github.com/ory/kratos/request" + "github.com/ory/kratos/schema" + "github.com/ory/x/otelx" +) + +type ( + PasswordMigration struct { + deps webHookDependencies + conf json.RawMessage + } + PasswordMigrationRequest struct { + Identifier string `json:"identifier"` + Password string `json:"password"` + } + PasswordMigrationResponse struct { + Status string `json:"status"` + } +) + +func NewPasswordMigrationHook(deps webHookDependencies, conf json.RawMessage) *PasswordMigration { + return &PasswordMigration{deps: deps, conf: conf} +} + +func (p *PasswordMigration) Execute(ctx context.Context, data *PasswordMigrationRequest) (err error) { + var ( + httpClient = p.deps.HTTPClient(ctx) + emitEvent = gjson.GetBytes(p.conf, "emit_analytics_event").Bool() || !gjson.GetBytes(p.conf, "emit_analytics_event").Exists() // default true + tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks") + ) + + ctx, span := tracer.Start(ctx, "selfservice.login.password_migration") + defer otelx.End(span, &err) + + if emitEvent { + instrumentHTTPClientForEvents(ctx, httpClient) + } + builder, err := request.NewBuilder(ctx, p.conf, p.deps, nil) + if err != nil { + return errors.WithStack(err) + } + req, err := builder.BuildRequest(ctx, nil) // passing a nil body here skips Jsonnet + if err != nil { + return errors.WithStack(err) + } + rawData, err := json.Marshal(data) + if err != nil { + return errors.WithStack(err) + } + if err = req.SetBody(rawData); err != nil { + return errors.WithStack(err) + } + + p.deps.Logger().WithRequest(req.Request).Info("Dispatching password migration hook") + req = req.WithContext(ctx) + + resp, err := httpClient.Do(req) + if err != nil { + return herodot.DefaultError{ + CodeField: http.StatusBadGateway, + StatusField: http.StatusText(http.StatusBadGateway), + GRPCCodeField: grpccodes.Aborted, + ReasonField: "A third-party upstream service could not be reached. Please try again later.", + ErrorField: "calling the password migration hook failed", + }.WithWrap(errors.WithStack(err)) + } + defer resp.Body.Close() + span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) + + switch resp.StatusCode { + case http.StatusOK: + // We now check if the response matches `{"status": "password_match" }`. + dec := json.NewDecoder(io.LimitReader(resp.Body, 1024)) // limit the response body to 1KB + var response PasswordMigrationResponse + if err := dec.Decode(&response); err != nil || response.Status != "password_match" { + return errors.WithStack(schema.NewInvalidCredentialsError()) + } + return nil + + case http.StatusForbidden: + return errors.WithStack(schema.NewInvalidCredentialsError()) + default: + span.SetStatus(codes.Error, "Unexpected HTTP status code") + return herodot.DefaultError{ + CodeField: http.StatusBadGateway, + StatusField: http.StatusText(http.StatusBadGateway), + GRPCCodeField: grpccodes.Aborted, + ReasonField: "A third-party upstream service responded improperly. Please try again later.", + ErrorField: fmt.Sprintf("password migration hook failed with status code %v", resp.StatusCode), + } + } +} diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 8c91d7e6c4f9..3600e29b4e0e 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -10,7 +10,9 @@ import ( "net/http" "time" + "github.com/ory/kratos/hash" "github.com/ory/kratos/selfservice/flowhelpers" + "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" "github.com/ory/x/stringsx" @@ -22,7 +24,6 @@ import ( "github.com/ory/herodot" "github.com/ory/x/decoderx" - "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" @@ -69,7 +70,8 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(w, r, f, &p, err) } - i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), stringsx.Coalesce(p.Identifier, p.LegacyIdentifier)) + identifier := stringsx.Coalesce(p.Identifier, p.LegacyIdentifier) + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), identifier) if err != nil { time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation)) return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) @@ -81,17 +83,33 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err) } - if err := hash.Compare(r.Context(), []byte(p.Password), []byte(o.HashedPassword)); err != nil { - return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) - } + if o.ShouldUsePasswordMigrationHook() { + pwHook := s.d.Config().PasswordMigrationHook(r.Context()) + if !pwHook.Enabled { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Password migration hook is not enabled but password migration is requested.")) + } + + migrationHook := hook.NewPasswordMigrationHook(s.d, pwHook.Config) + err = migrationHook.Execute(r.Context(), &hook.PasswordMigrationRequest{Identifier: identifier, Password: p.Password}) + if err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } - if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) { if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil { return nil, s.handleLoginError(w, r, f, &p, err) } + } else { + if err := hash.Compare(r.Context(), []byte(p.Password), []byte(o.HashedPassword)); err != nil { + return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) + } + + if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) { + if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + } } - f.Active = identity.CredentialsTypePassword f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index 8d8879cce91c..8c2f2cb73245 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -16,34 +16,30 @@ import ( "testing" "time" - "github.com/ory/kratos/driver" - "github.com/ory/kratos/internal/registrationhelpers" - - "github.com/ory/kratos/selfservice/flow" - + "github.com/gobuffalo/httptest" "github.com/gofrs/uuid" - - "github.com/ory/x/urlx" - - "github.com/ory/kratos/hash" - kratos "github.com/ory/kratos/internal/httpclient" - "github.com/ory/x/assertx" - "github.com/ory/x/errorsx" - "github.com/ory/x/ioutilx" - "github.com/ory/x/sqlxx" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" + "github.com/ory/kratos/internal/registrationhelpers" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/text" "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/errorsx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) //go:embed stub/login.schema.json @@ -864,4 +860,207 @@ func TestCompleteLogin(t *testing.T) { false, true, http.StatusOK, redirTS.URL) assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) }) + + t.Run("suite=password migration hook", func(t *testing.T) { + ctx := context.Background() + + type ( + hookPayload = struct { + Identifier string `json:"identifier"` + Password string `json:"password"` + } + tsRequestHandler = func(hookPayload) (status int, body string) + ) + returnStatus := func(status int) func(string, string) tsRequestHandler { + return func(string, string) tsRequestHandler { + return func(hookPayload) (int, string) { return status, "" } + } + } + returnStatic := func(status int, body string) func(string, string) tsRequestHandler { + return func(string, string) tsRequestHandler { + return func(hookPayload) (int, string) { return status, body } + } + } + + // each test case sends (number of expected calls) handlers to the channel, at a max of 3 + tsChan := make(chan tsRequestHandler, 3) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + _ = r.Body.Close() + var payload hookPayload + require.NoError(t, json.Unmarshal(b, &payload)) + + select { + case handlerFn := <-tsChan: + status, body := handlerFn(payload) + w.WriteHeader(status) + _, _ = io.WriteString(w, body) + + default: + t.Fatal("unexpected call to the password migration hook") + } + })) + t.Cleanup(ts.Close) + + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook, &config.PasswordMigrationHook{ + Enabled: true, + Config: json.RawMessage(fmt.Sprintf(`{"URL":"%s"}`, ts.URL)), + })) + + for _, tc := range []struct { + name string + hookHandler func(identifier, password string) tsRequestHandler + expectHookCalls int + setupFn func() func() + credentialsConfig string + expectSuccess bool + }{{ + name: "should call migration hook", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: func(identifier, password string) tsRequestHandler { + return func(payload hookPayload) (status int, body string) { + if payload.Identifier == identifier && payload.Password == password { + return http.StatusOK, `{"status":"password_match"}` + } else { + return http.StatusOK, `{"status":"no_match"}` + } + } + }, + expectHookCalls: 1, + expectSuccess: true, + }, { + name: "should not update identity when the password is wrong", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusForbidden), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should inspect response", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatic(http.StatusOK, `{"status":"password_no_match"}`), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 200 without JSON", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusOK), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 500", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusInternalServerError), + expectHookCalls: 3, // expect retries on 500 + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 201", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatic(http.StatusCreated, `{"status":"password_match"}`), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when hash is set", + credentialsConfig: `{"use_password_migration_hook": true, "hashed_password":"hash"}`, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when use_password_migration_hook is not set", + credentialsConfig: `{"hashed_password":"hash"}`, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when credential is empty", + credentialsConfig: `{}`, + expectSuccess: false, + }, { + name: "should not call migration hook if disabled", + credentialsConfig: `{"use_password_migration_hook": true}`, + setupFn: func() func() { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", false)) + return func() { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", true)) + } + }, + expectSuccess: false, + }} { + + t.Run("case="+tc.name, func(t *testing.T) { + if tc.setupFn != nil { + cleanup := tc.setupFn() + t.Cleanup(cleanup) + } + + identifier := x.NewUUID().String() + password := x.NewUUID().String() + iId := x.NewUUID() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &identity.Identity{ + ID: iId, + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(tc.credentialsConfig), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: identifier, + Verified: true, + CreatedAt: time.Now(), + IdentityID: iId, + }, + }, + })) + + values := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("method", identity.CredentialsTypePassword.String()) + v.Set("password", password) + } + + for range tc.expectHookCalls { + tsChan <- tc.hookHandler(identifier, password) + } + + browserClient := testhelpers.NewClientWithCookies(t) + + if tc.expectSuccess { + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, false, http.StatusOK, redirTS.URL) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + + // check if password hash algorithm is upgraded + _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, identifier) + require.NoError(t, err) + var o identity.CredentialsPassword + require.NoError(t, json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o)) + assert.True(t, reg.Hasher(ctx).Understands([]byte(o.HashedPassword)), "%s", o.HashedPassword) + assert.True(t, hash.IsBcryptHash([]byte(o.HashedPassword)), "%s", o.HashedPassword) + + // retry after upgraded + body = testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, true, http.StatusOK, redirTS.URL) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + } else { + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, false, http.StatusOK, "") + assert.Empty(t, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + // Check that the config did not change + _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypePassword, identifier) + require.NoError(t, err) + assert.JSONEq(t, tc.credentialsConfig, string(c.Config)) + } + + // expect all hook calls to be done + select { + case <-tsChan: + t.Fatal("the test unexpectedly did too few calls to the password hook") + default: + // pass + } + }) + } + }) } diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go index 911ad619cd15..ae57982dd89f 100644 --- a/selfservice/strategy/password/strategy.go +++ b/selfservice/strategy/password/strategy.go @@ -7,11 +7,12 @@ import ( "context" "encoding/json" - "github.com/ory/kratos/ui/node" - "github.com/go-playground/validator/v10" "github.com/pkg/errors" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/decoderx" "github.com/ory/kratos/continuity" @@ -37,9 +38,10 @@ type registrationStrategyDependencies interface { x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider - + x.HTTPClientProvider + x.TracingProvider + jsonnetsecure.VMProvider config.Provider - continuity.ManagementProvider errorx.ManagementProvider From 020a9dea054fa6e5bad1ec253f2fe346a490131d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 3 Jul 2024 07:29:21 +0000 Subject: [PATCH 130/262] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - .../model_identity_credentials_password.go | 37 +++++++++++++++++++ .../model_identity_credentials_password.go | 37 +++++++++++++++++++ spec/api.json | 4 ++ spec/swagger.json | 4 ++ 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_identity_credentials_password.go b/internal/client-go/model_identity_credentials_password.go index 85f942fb6852..df1900568bb3 100644 --- a/internal/client-go/model_identity_credentials_password.go +++ b/internal/client-go/model_identity_credentials_password.go @@ -19,6 +19,8 @@ import ( type IdentityCredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword *string `json:"hashed_password,omitempty"` + // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object @@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) { o.HashedPassword = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { toSerialize["hashed_password"] = o.HashedPassword } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_identity_credentials_password.go b/internal/httpclient/model_identity_credentials_password.go index 85f942fb6852..df1900568bb3 100644 --- a/internal/httpclient/model_identity_credentials_password.go +++ b/internal/httpclient/model_identity_credentials_password.go @@ -19,6 +19,8 @@ import ( type IdentityCredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword *string `json:"hashed_password,omitempty"` + // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object @@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) { o.HashedPassword = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { toSerialize["hashed_password"] = o.HashedPassword } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/spec/api.json b/spec/api.json index f8a84f6f7d97..1bb1345dc25c 100644 --- a/spec/api.json +++ b/spec/api.json @@ -1080,6 +1080,10 @@ "hashed_password": { "description": "HashedPassword is a hash-representation of the password.", "type": "string" + }, + "use_password_migration_hook": { + "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.", + "type": "boolean" } }, "title": "CredentialsPassword is contains the configuration for credentials of the type password.", diff --git a/spec/swagger.json b/spec/swagger.json index 570cb4003d62..e790c71fecb4 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -4211,6 +4211,10 @@ "hashed_password": { "description": "HashedPassword is a hash-representation of the password.", "type": "string" + }, + "use_password_migration_hook": { + "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.", + "type": "boolean" } } }, From 180287a3153042f586319208b314386074be7554 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 4 Jul 2024 11:48:18 +0200 Subject: [PATCH 131/262] chore: use label in link/unlink settings nodes (#3977) --- ..._on_linked_credentials-agent=githuber.json | 4 +- ...on_linked_credentials-agent=multiuser.json | 4 +- ..._on_linked_credentials-agent=password.json | 4 +- ...e=should_link_a_connection-flow=fetch.json | 4 +- ...hould_link_a_connection-flow=original.json | 4 +- ...hould_link_a_connection-flow=response.json | 4 +- ...er_does_not_have_oidc_credentials_yet.json | 4 +- ...ink_a_connection_which_already_exists.json | 4 +- ..._connection_not_yet_linked-flow=fetch.json | 4 +- ...a_connection_not_yet_linked-flow=json.json | 4 +- ...an_non-existing_connection-flow=fetch.json | 4 +- ..._an_non-existing_connection-flow=json.json | 4 +- selfservice/strategy/oidc/nodes.go | 8 +- .../strategy/oidc/strategy_settings.go | 5 +- .../strategy/oidc/strategy_settings_test.go | 123 ++++++++++++------ .../profiles/oidc/login/success.spec.ts | 2 +- .../profiles/oidc/settings/success.spec.ts | 2 +- 17 files changed, 115 insertions(+), 73 deletions(-) diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json index dd0dc9e5f179..3b534e240899 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json index 55909b7380a6..94db74f27534 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050002, - "text": "Link ory", + "text": "Link Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json index b775cb07f8b3..37108bfe985a 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json index cda03ca13acb..fc364efa9d90 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json index 1763aae80238..cc010fb2c206 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050002, - "text": "Link ory", + "text": "Link Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json index a8b9407aab8a..ddaaf6905c12 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json index a8b9407aab8a..ddaaf6905c12 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json index a8b9407aab8a..ddaaf6905c12 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/nodes.go b/selfservice/strategy/oidc/nodes.go index e60dd6324d1a..3dc725e0d967 100644 --- a/selfservice/strategy/oidc/nodes.go +++ b/selfservice/strategy/oidc/nodes.go @@ -8,10 +8,10 @@ import ( "github.com/ory/kratos/ui/node" ) -func NewLinkNode(provider string) *node.Node { - return node.NewInputField("link", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(provider)) +func NewLinkNode(providerID, providerLabel string) *node.Node { + return node.NewInputField("link", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(providerLabel)) } -func NewUnlinkNode(provider string) *node.Node { - return node.NewInputField("unlink", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(provider)) +func NewUnlinkNode(providerID, providerLabel string) *node.Node { + return node.NewInputField("unlink", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(providerLabel)) } diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 4fde3a457548..d4a92056a20b 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" "github.com/tidwall/sjson" @@ -173,7 +174,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity if l.Config().OrganizationID != "" { continue } - sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID)) + sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID))) } count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), confidential) @@ -185,7 +186,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity // This means that we're able to remove a connection because it is the last configured credential. If it is // removed, the identity is no longer able to sign in. for _, l := range linked { - sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID)) + sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID))) } } diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index 753bae5321b8..65e5ab30600c 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -7,6 +7,7 @@ import ( "context" _ "embed" "encoding/json" + "fmt" "net/http" "net/url" "strconv" @@ -15,6 +16,7 @@ import ( "github.com/ory/x/snapshotx" + "github.com/ory/kratos/driver" kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" @@ -28,7 +30,6 @@ import ( "github.com/ory/x/sqlxx" - "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" @@ -36,6 +37,7 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/ory/kratos/selfservice/strategy/oidc" "github.com/ory/kratos/x" ) @@ -67,7 +69,9 @@ func TestSettingsStrategy(t *testing.T) { viperSetProviderConfig( t, conf, - newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory"), + newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory", func(c *oidc.Configuration) { + c.Label = "Ory" + }), newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "google"), newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "github"), orgSSO, @@ -609,21 +613,27 @@ func TestSettingsStrategy(t *testing.T) { } func TestPopulateSettingsMethod(t *testing.T) { - ctx := context.Background() - nreg := func(t *testing.T, conf *oidc.ConfigurationCollection) *driver.RegistryDefault { - c, reg := internal.NewFastRegistryWithMocks(t) - - testhelpers.SetDefaultIdentitySchema(c, "file://stub/registration.schema.json") - c.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + t.Parallel() + nCtx := func(t *testing.T, conf *oidc.ConfigurationCollection) (*driver.RegistryDefault, context.Context) { + _, reg := internal.NewFastRegistryWithMocks(t) + ctx := context.Background() + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json") + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + baseKey := fmt.Sprintf("%s.%s", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeOIDC) + + ctx = confighelpers.WithConfigValues(ctx, map[string]interface{}{ + baseKey + ".enabled": true, + baseKey + ".config": conf, + }) // Enabled per default: // conf.Set(ctx, configuration.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) - viperSetProviderConfig(t, c, conf.Providers...) - return reg + // viperSetProviderConfig(t, c, conf.Providers...) + return reg, ctx } - ns := func(t *testing.T, reg *driver.RegistryDefault) *oidc.Strategy { - ss, err := reg.SettingsStrategies(context.Background()).Strategy(identity.CredentialsTypeOIDC.String()) + ns := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context) *oidc.Strategy { + ss, err := reg.SettingsStrategies(ctx).Strategy(identity.CredentialsTypeOIDC.String()) require.NoError(t, err) return ss.(*oidc.Strategy) } @@ -632,13 +642,14 @@ func TestPopulateSettingsMethod(t *testing.T) { return &settings.Flow{Type: flow.TypeBrowser, ID: x.NewUUID(), UI: container.New("")} } - populate := func(t *testing.T, reg *driver.RegistryDefault, i *identity.Identity, req *settings.Flow) *container.Container { - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req)) - require.NotNil(t, req.UI) - require.NotNil(t, req.UI.Nodes) - assert.Equal(t, "POST", req.UI.Method) - return req.UI + populate := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context, i *identity.Identity, f *settings.Flow) *container.Container { + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + req := new(http.Request) + require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f)) + require.NotNil(t, f.UI) + require.NotNil(t, f.UI.Nodes) + assert.Equal(t, "POST", f.UI.Method) + return f.UI } defaultConfig := []oidc.Configuration{ @@ -648,12 +659,14 @@ func TestPopulateSettingsMethod(t *testing.T) { } t.Run("case=should not populate non-browser flow", func(t *testing.T) { - reg := nreg(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}}) + t.Parallel() + reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}}) i := &identity.Identity{Traits: []byte(`{"subject":"foo@bar.com"}`)} - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - req := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")} - require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req)) - require.Empty(t, req.UI.Nodes) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + f := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")} + req := new(http.Request) + require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f)) + require.Empty(t, f.UI.Nodes) }) for k, tc := range []struct { @@ -674,25 +687,25 @@ func TestPopulateSettingsMethod(t *testing.T) { }, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("github", "github"), }, }, { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("google"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), }, }, { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("google"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), }, i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{}, Config: []byte(`{}`)}, }, @@ -700,8 +713,8 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), }, i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ "google:1234", @@ -711,9 +724,9 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("github"), - oidc.NewUnlinkNode("google"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), }, withpw: true, i: &identity.Credentials{ @@ -727,9 +740,9 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("github"), - oidc.NewUnlinkNode("google"), - oidc.NewUnlinkNode("facebook"), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), + oidc.NewUnlinkNode("facebook", "facebook"), }, i: &identity.Credentials{ Type: identity.CredentialsTypeOIDC, Identifiers: []string{ @@ -739,9 +752,37 @@ func TestPopulateSettingsMethod(t *testing.T) { Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), }, }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "labeled", Label: "Labeled"}, + }, + e: node.Nodes{ + node.NewCSRFNode(x.FakeCSRFToken), + oidc.NewLinkNode("labeled", "Labeled"), + }, + }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "labeled", Label: "Labeled"}, + {Provider: "generic", ID: "facebook"}, + }, + e: node.Nodes{ + node.NewCSRFNode(x.FakeCSRFToken), + oidc.NewUnlinkNode("labeled", "Labeled"), + oidc.NewUnlinkNode("facebook", "facebook"), + }, + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "labeled:1234", + "facebook:1234", + }, + Config: []byte(`{"providers":[{"provider":"labeled","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), + }, + }, } { t.Run("iteration="+strconv.Itoa(k), func(t *testing.T) { - reg := nreg(t, &oidc.ConfigurationCollection{Providers: tc.c}) + t.Parallel() + reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: tc.c}) i := &identity.Identity{ Traits: []byte(`{"subject":"foo@bar.com"}`), Credentials: make(map[identity.CredentialsType]identity.Credentials, 2), @@ -756,7 +797,7 @@ func TestPopulateSettingsMethod(t *testing.T) { Config: []byte(`{"hashed_password":"$argon2id$..."}`), } } - actual := populate(t, reg, i, nr()) + actual := populate(t, reg, ctx, i, nr()) assert.EqualValues(t, tc.e, actual.Nodes) }) } diff --git a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts index 8e381e7acf5d..153b3332dd81 100644 --- a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts @@ -70,7 +70,7 @@ context("Social Sign In Successes", () => { cy.visit(settings) cy.get('[value="hydra"]') .should("have.attr", "name", "unlink") - .should("contain.text", "Unlink hydra") + .should("contain.text", "Unlink Ory") }) it("should be able to sign up with redirects", () => { diff --git a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts index 674e24e0d668..5f22ae886338 100644 --- a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts @@ -94,7 +94,7 @@ context("Social Sign In Settings Success", () => { cy.get('[value="hydra"]') .should("have.attr", "name", "unlink") - .should("contain.text", "Unlink hydra") + .should("contain.text", "Unlink Ory") }) it("should link google", () => { From 7674f46c86fb900f1e2150ccfb721b0bf3f23d88 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:38:23 +0000 Subject: [PATCH 132/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 531 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 365 insertions(+), 166 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58628071c945..41491e3bebf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,286 +5,294 @@ **Table of Contents** -- [ (2024-04-26)](#2024-04-26) +- [ (2024-07-04)](#2024-07-04) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) + - [Documentation](#documentation) - [Features](#features) - [Tests](#tests) - - [Unclassified](#unclassified) -- [1.1.0 (2024-02-20)](#110-2024-02-20) +- [1.2.0 (2024-06-05)](#120-2024-06-05) - [Breaking Changes](#breaking-changes-1) - [Bug Fixes](#bug-fixes-1) - [Code Generation](#code-generation) - - [Documentation](#documentation) + - [Documentation](#documentation-1) - [Features](#features-1) - - [Reverts](#reverts) - [Tests](#tests-1) + - [Unclassified](#unclassified) +- [1.1.0 (2024-02-20)](#110-2024-02-20) + - [Breaking Changes](#breaking-changes-2) + - [Bug Fixes](#bug-fixes-2) + - [Code Generation](#code-generation-1) + - [Documentation](#documentation-2) + - [Features](#features-2) + - [Reverts](#reverts) + - [Tests](#tests-2) - [Unclassified](#unclassified-1) - [1.0.0 (2023-07-12)](#100-2023-07-12) - - [Bug Fixes](#bug-fixes-2) - - [Code Generation](#code-generation-1) - - [Documentation](#documentation-1) - - [Features](#features-2) - - [Tests](#tests-2) + - [Bug Fixes](#bug-fixes-3) + - [Code Generation](#code-generation-2) + - [Documentation](#documentation-3) + - [Features](#features-3) + - [Tests](#tests-3) - [Unclassified](#unclassified-2) - [0.13.0 (2023-04-18)](#0130-2023-04-18) - - [Breaking Changes](#breaking-changes-2) - - [Bug Fixes](#bug-fixes-3) - - [Code Generation](#code-generation-2) - - [Code Refactoring](#code-refactoring) - - [Documentation](#documentation-2) - - [Features](#features-3) - - [Tests](#tests-3) - - [Unclassified](#unclassified-3) -- [0.11.1 (2023-01-14)](#0111-2023-01-14) - [Breaking Changes](#breaking-changes-3) - [Bug Fixes](#bug-fixes-4) - [Code Generation](#code-generation-3) - - [Documentation](#documentation-3) + - [Code Refactoring](#code-refactoring) + - [Documentation](#documentation-4) - [Features](#features-4) - [Tests](#tests-4) -- [0.11.0 (2022-12-02)](#0110-2022-12-02) - - [Code Generation](#code-generation-4) - - [Features](#features-5) -- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28) + - [Unclassified](#unclassified-3) +- [0.11.1 (2023-01-14)](#0111-2023-01-14) - [Breaking Changes](#breaking-changes-4) - [Bug Fixes](#bug-fixes-5) - - [Code Generation](#code-generation-5) + - [Code Generation](#code-generation-4) + - [Documentation](#documentation-5) + - [Features](#features-5) + - [Tests](#tests-5) +- [0.11.0 (2022-12-02)](#0110-2022-12-02) + - [Code Generation](#code-generation-5) + - [Features](#features-6) +- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28) + - [Breaking Changes](#breaking-changes-5) + - [Bug Fixes](#bug-fixes-6) + - [Code Generation](#code-generation-6) - [Code Refactoring](#code-refactoring-1) - - [Documentation](#documentation-4) - - [Features](#features-6) + - [Documentation](#documentation-6) + - [Features](#features-7) - [Reverts](#reverts-1) - - [Tests](#tests-5) + - [Tests](#tests-6) - [Unclassified](#unclassified-4) - [0.10.1 (2022-06-01)](#0101-2022-06-01) - - [Bug Fixes](#bug-fixes-6) - - [Code Generation](#code-generation-6) + - [Bug Fixes](#bug-fixes-7) + - [Code Generation](#code-generation-7) - [0.10.0 (2022-05-30)](#0100-2022-05-30) - - [Breaking Changes](#breaking-changes-5) - - [Bug Fixes](#bug-fixes-7) - - [Code Generation](#code-generation-7) - - [Code Refactoring](#code-refactoring-2) - - [Documentation](#documentation-5) - - [Features](#features-7) - - [Tests](#tests-6) - - [Unclassified](#unclassified-5) -- [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25) - [Breaking Changes](#breaking-changes-6) - [Bug Fixes](#bug-fixes-8) - [Code Generation](#code-generation-8) - - [Documentation](#documentation-6) -- [0.9.0-alpha.2 (2022-03-22)](#090-alpha2-2022-03-22) - - [Bug Fixes](#bug-fixes-9) - - [Code Generation](#code-generation-9) -- [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21) - - [Breaking Changes](#breaking-changes-7) - - [Bug Fixes](#bug-fixes-10) - - [Code Generation](#code-generation-10) - - [Code Refactoring](#code-refactoring-3) + - [Code Refactoring](#code-refactoring-2) - [Documentation](#documentation-7) - [Features](#features-8) - [Tests](#tests-7) - - [Unclassified](#unclassified-6) -- [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21) + - [Unclassified](#unclassified-5) +- [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25) + - [Breaking Changes](#breaking-changes-7) + - [Bug Fixes](#bug-fixes-9) + - [Code Generation](#code-generation-9) + - [Documentation](#documentation-8) +- [0.9.0-alpha.2 (2022-03-22)](#090-alpha2-2022-03-22) + - [Bug Fixes](#bug-fixes-10) + - [Code Generation](#code-generation-10) +- [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21) - [Breaking Changes](#breaking-changes-8) - [Bug Fixes](#bug-fixes-11) - [Code Generation](#code-generation-11) - - [Code Refactoring](#code-refactoring-4) - - [Documentation](#documentation-8) + - [Code Refactoring](#code-refactoring-3) + - [Documentation](#documentation-9) - [Features](#features-9) - [Tests](#tests-8) + - [Unclassified](#unclassified-6) +- [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21) + - [Breaking Changes](#breaking-changes-9) + - [Bug Fixes](#bug-fixes-12) + - [Code Generation](#code-generation-12) + - [Code Refactoring](#code-refactoring-4) + - [Documentation](#documentation-10) + - [Features](#features-10) + - [Tests](#tests-9) - [0.8.2-alpha.1 (2021-12-17)](#082-alpha1-2021-12-17) - - [Bug Fixes](#bug-fixes-12) - - [Code Generation](#code-generation-12) - - [Documentation](#documentation-9) -- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13) - [Bug Fixes](#bug-fixes-13) - [Code Generation](#code-generation-13) - - [Documentation](#documentation-10) - - [Features](#features-10) - - [Tests](#tests-9) + - [Documentation](#documentation-11) +- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13) + - [Bug Fixes](#bug-fixes-14) + - [Code Generation](#code-generation-14) + - [Documentation](#documentation-12) + - [Features](#features-11) + - [Tests](#tests-10) - [0.8.0-alpha.4.pre.0 (2021-11-09)](#080-alpha4pre0-2021-11-09) - - [Breaking Changes](#breaking-changes-9) - - [Bug Fixes](#bug-fixes-14) - - [Code Generation](#code-generation-14) - - [Documentation](#documentation-11) - - [Features](#features-11) - - [Tests](#tests-10) + - [Breaking Changes](#breaking-changes-10) + - [Bug Fixes](#bug-fixes-15) + - [Code Generation](#code-generation-15) + - [Documentation](#documentation-13) + - [Features](#features-12) + - [Tests](#tests-11) - [0.8.0-alpha.3 (2021-10-28)](#080-alpha3-2021-10-28) - - [Bug Fixes](#bug-fixes-15) - - [Code Generation](#code-generation-15) -- [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28) + - [Bug Fixes](#bug-fixes-16) - [Code Generation](#code-generation-16) +- [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28) + - [Code Generation](#code-generation-17) - [0.8.0-alpha.1 (2021-10-27)](#080-alpha1-2021-10-27) - - [Breaking Changes](#breaking-changes-10) - - [Bug Fixes](#bug-fixes-16) - - [Code Generation](#code-generation-17) + - [Breaking Changes](#breaking-changes-11) + - [Bug Fixes](#bug-fixes-17) + - [Code Generation](#code-generation-18) - [Code Refactoring](#code-refactoring-5) - - [Documentation](#documentation-12) - - [Features](#features-12) + - [Documentation](#documentation-14) + - [Features](#features-13) - [Reverts](#reverts-2) - - [Tests](#tests-11) + - [Tests](#tests-12) - [Unclassified](#unclassified-7) - [0.7.6-alpha.1 (2021-09-12)](#076-alpha1-2021-09-12) - - [Code Generation](#code-generation-18) -- [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11) - [Code Generation](#code-generation-19) -- [0.7.4-alpha.1 (2021-09-09)](#074-alpha1-2021-09-09) - - [Bug Fixes](#bug-fixes-17) +- [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11) - [Code Generation](#code-generation-20) - - [Documentation](#documentation-13) - - [Features](#features-13) - - [Tests](#tests-12) -- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28) +- [0.7.4-alpha.1 (2021-09-09)](#074-alpha1-2021-09-09) - [Bug Fixes](#bug-fixes-18) - [Code Generation](#code-generation-21) - - [Documentation](#documentation-14) + - [Documentation](#documentation-15) - [Features](#features-14) -- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22) + - [Tests](#tests-13) +- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28) - [Bug Fixes](#bug-fixes-19) - [Code Generation](#code-generation-22) - - [Documentation](#documentation-15) - - [Tests](#tests-13) + - [Documentation](#documentation-16) + - [Features](#features-15) +- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22) + - [Bug Fixes](#bug-fixes-20) + - [Code Generation](#code-generation-23) + - [Documentation](#documentation-17) + - [Tests](#tests-14) - [0.7.0-alpha.1 (2021-07-13)](#070-alpha1-2021-07-13) - - [Breaking Changes](#breaking-changes-11) - - [Bug Fixes](#bug-fixes-20) - - [Code Generation](#code-generation-23) - - [Code Refactoring](#code-refactoring-6) - - [Documentation](#documentation-16) - - [Features](#features-15) - - [Tests](#tests-14) - - [Unclassified](#unclassified-8) -- [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17) - [Breaking Changes](#breaking-changes-12) - [Bug Fixes](#bug-fixes-21) - [Code Generation](#code-generation-24) + - [Code Refactoring](#code-refactoring-6) + - [Documentation](#documentation-18) + - [Features](#features-16) + - [Tests](#tests-15) + - [Unclassified](#unclassified-8) +- [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17) + - [Breaking Changes](#breaking-changes-13) + - [Bug Fixes](#bug-fixes-22) + - [Code Generation](#code-generation-25) - [Code Refactoring](#code-refactoring-7) - [0.6.2-alpha.1 (2021-05-14)](#062-alpha1-2021-05-14) - - [Code Generation](#code-generation-25) - - [Documentation](#documentation-17) -- [0.6.1-alpha.1 (2021-05-11)](#061-alpha1-2021-05-11) - [Code Generation](#code-generation-26) - - [Features](#features-16) -- [0.6.0-alpha.2 (2021-05-07)](#060-alpha2-2021-05-07) - - [Bug Fixes](#bug-fixes-22) + - [Documentation](#documentation-19) +- [0.6.1-alpha.1 (2021-05-11)](#061-alpha1-2021-05-11) - [Code Generation](#code-generation-27) - [Features](#features-17) +- [0.6.0-alpha.2 (2021-05-07)](#060-alpha2-2021-05-07) + - [Bug Fixes](#bug-fixes-23) + - [Code Generation](#code-generation-28) + - [Features](#features-18) - [0.6.0-alpha.1 (2021-05-05)](#060-alpha1-2021-05-05) - - [Breaking Changes](#breaking-changes-13) - - [Bug Fixes](#bug-fixes-23) - - [Code Generation](#code-generation-28) + - [Breaking Changes](#breaking-changes-14) + - [Bug Fixes](#bug-fixes-24) + - [Code Generation](#code-generation-29) - [Code Refactoring](#code-refactoring-8) - - [Documentation](#documentation-18) - - [Features](#features-18) - - [Tests](#tests-15) + - [Documentation](#documentation-20) + - [Features](#features-19) + - [Tests](#tests-16) - [Unclassified](#unclassified-9) - [0.5.5-alpha.1 (2020-12-09)](#055-alpha1-2020-12-09) - - [Bug Fixes](#bug-fixes-24) - - [Code Generation](#code-generation-29) - - [Documentation](#documentation-19) - - [Features](#features-19) - - [Tests](#tests-16) - - [Unclassified](#unclassified-10) -- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11) - [Bug Fixes](#bug-fixes-25) - [Code Generation](#code-generation-30) - - [Code Refactoring](#code-refactoring-9) - - [Documentation](#documentation-20) + - [Documentation](#documentation-21) - [Features](#features-20) -- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27) + - [Tests](#tests-17) + - [Unclassified](#unclassified-10) +- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11) - [Bug Fixes](#bug-fixes-26) - [Code Generation](#code-generation-31) - - [Documentation](#documentation-21) + - [Code Refactoring](#code-refactoring-9) + - [Documentation](#documentation-22) - [Features](#features-21) - - [Tests](#tests-17) -- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22) +- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27) - [Bug Fixes](#bug-fixes-27) - [Code Generation](#code-generation-32) - - [Documentation](#documentation-22) + - [Documentation](#documentation-23) + - [Features](#features-22) - [Tests](#tests-18) -- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20) +- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22) - [Bug Fixes](#bug-fixes-28) - [Code Generation](#code-generation-33) - - [Documentation](#documentation-23) - - [Features](#features-22) + - [Documentation](#documentation-24) - [Tests](#tests-19) +- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20) + - [Bug Fixes](#bug-fixes-29) + - [Code Generation](#code-generation-34) + - [Documentation](#documentation-25) + - [Features](#features-23) + - [Tests](#tests-20) - [Unclassified](#unclassified-11) - [0.5.0-alpha.1 (2020-10-15)](#050-alpha1-2020-10-15) - - [Breaking Changes](#breaking-changes-14) - - [Bug Fixes](#bug-fixes-29) - - [Code Generation](#code-generation-34) + - [Breaking Changes](#breaking-changes-15) + - [Bug Fixes](#bug-fixes-30) + - [Code Generation](#code-generation-35) - [Code Refactoring](#code-refactoring-10) - - [Documentation](#documentation-24) - - [Features](#features-23) - - [Tests](#tests-20) + - [Documentation](#documentation-26) + - [Features](#features-24) + - [Tests](#tests-21) - [Unclassified](#unclassified-12) - [0.4.6-alpha.1 (2020-07-13)](#046-alpha1-2020-07-13) - - [Bug Fixes](#bug-fixes-30) - - [Code Generation](#code-generation-35) -- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13) - [Bug Fixes](#bug-fixes-31) - [Code Generation](#code-generation-36) -- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10) +- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13) - [Bug Fixes](#bug-fixes-32) - [Code Generation](#code-generation-37) - - [Documentation](#documentation-25) -- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08) +- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10) - [Bug Fixes](#bug-fixes-33) - [Code Generation](#code-generation-38) -- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08) + - [Documentation](#documentation-27) +- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08) - [Bug Fixes](#bug-fixes-34) - [Code Generation](#code-generation-39) +- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08) + - [Bug Fixes](#bug-fixes-35) + - [Code Generation](#code-generation-40) - [0.4.0-alpha.1 (2020-07-08)](#040-alpha1-2020-07-08) - - [Breaking Changes](#breaking-changes-15) - - [Bug Fixes](#bug-fixes-35) - - [Code Generation](#code-generation-40) + - [Breaking Changes](#breaking-changes-16) + - [Bug Fixes](#bug-fixes-36) + - [Code Generation](#code-generation-41) - [Code Refactoring](#code-refactoring-11) - - [Documentation](#documentation-26) - - [Features](#features-24) + - [Documentation](#documentation-28) + - [Features](#features-25) - [Unclassified](#unclassified-13) - [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15) - - [Breaking Changes](#breaking-changes-16) - - [Bug Fixes](#bug-fixes-36) + - [Breaking Changes](#breaking-changes-17) + - [Bug Fixes](#bug-fixes-37) - [Chores](#chores) - [Code Refactoring](#code-refactoring-12) - - [Documentation](#documentation-27) - - [Features](#features-25) + - [Documentation](#documentation-29) + - [Features](#features-26) - [Unclassified](#unclassified-14) - [0.2.1-alpha.1 (2020-05-05)](#021-alpha1-2020-05-05) - [Chores](#chores-1) - - [Documentation](#documentation-28) + - [Documentation](#documentation-30) - [0.2.0-alpha.2 (2020-05-04)](#020-alpha2-2020-05-04) - - [Breaking Changes](#breaking-changes-17) - - [Bug Fixes](#bug-fixes-37) + - [Breaking Changes](#breaking-changes-18) + - [Bug Fixes](#bug-fixes-38) - [Chores](#chores-2) - [Code Refactoring](#code-refactoring-13) - - [Documentation](#documentation-29) - - [Features](#features-26) + - [Documentation](#documentation-31) + - [Features](#features-27) - [Unclassified](#unclassified-15) - [0.1.1-alpha.1 (2020-02-18)](#011-alpha1-2020-02-18) - - [Bug Fixes](#bug-fixes-38) + - [Bug Fixes](#bug-fixes-39) - [Code Refactoring](#code-refactoring-14) - - [Documentation](#documentation-30) + - [Documentation](#documentation-32) - [0.1.0-alpha.6 (2020-02-16)](#010-alpha6-2020-02-16) - - [Bug Fixes](#bug-fixes-39) + - [Bug Fixes](#bug-fixes-40) - [Code Refactoring](#code-refactoring-15) - - [Documentation](#documentation-31) - - [Features](#features-27) -- [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06) - - [Documentation](#documentation-32) + - [Documentation](#documentation-33) - [Features](#features-28) +- [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06) + - [Documentation](#documentation-34) + - [Features](#features-29) - [0.1.0-alpha.4 (2020-02-06)](#010-alpha4-2020-02-06) - [Continuous Integration](#continuous-integration) - - [Documentation](#documentation-33) + - [Documentation](#documentation-35) - [0.1.0-alpha.3 (2020-02-06)](#010-alpha3-2020-02-06) - [Continuous Integration](#continuous-integration-1) - [0.1.0-alpha.2 (2020-02-03)](#010-alpha2-2020-02-03) - - [Bug Fixes](#bug-fixes-40) - - [Documentation](#documentation-34) - - [Features](#features-29) + - [Bug Fixes](#bug-fixes-41) + - [Documentation](#documentation-36) + - [Features](#features-30) - [Unclassified](#unclassified-16) - [0.1.0-alpha.1 (2020-01-31)](#010-alpha1-2020-01-31) - - [Documentation](#documentation-35) + - [Documentation](#documentation-37) - [0.0.3-alpha.15 (2020-01-31)](#003-alpha15-2020-01-31) - [Unclassified](#unclassified-17) - [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31) @@ -317,19 +325,172 @@ - [Unclassified](#unclassified-28) - [0.0.1-alpha.3 (2020-01-28)](#001-alpha3-2020-01-28) - [Continuous Integration](#continuous-integration-6) - - [Documentation](#documentation-36) + - [Documentation](#documentation-38) - [Unclassified](#unclassified-29) -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-26) +# [](https://github.com/ory/kratos/compare/v1.2.0...v) (2024-07-04) + +## Breaking Changes + +Going forward, the `/admin/session/.../extend` endpoint will return 204 no +content for new Ory Network projects. We will deprecate returning 200 + session +body in the future. + +### Bug Fixes + +- Jsonnet timeouts ([#3979](https://github.com/ory/kratos/issues/3979)) + ([7c5299f](https://github.com/ory/kratos/commit/7c5299f1f832ebbe0622d0920b7a91253d26b06c)) + +### Documentation + +- Typo in changelog + ([c508980](https://github.com/ory/kratos/commit/c5089801af2a656e9c1fc371a11aeb23918ba359)) + +### Features + +- Allow deletion of an individual OIDC credential + ([#3968](https://github.com/ory/kratos/issues/3968)) + ([a43cef2](https://github.com/ory/kratos/commit/a43cef23c177acddbf8b03afef087feeaca51981)): + + This extends the existing `DELETE /admin/identities/{id}/credentials/{type}` + API to accept an `?identifier=foobar` query parameter for `{type}==oidc` like + such: + + `DELETE /admin/identities/{id}/credentials/oidc?identifier=github%3A012345` + + This will delete the GitHub OIDC credential with the identifier + `github:012345` (`012345` is the subject as returned by GitHub). + + To find out which OIDC credentials exist, call + `GET /admin/identities/{id}?include_credential=oidc` beforehand. + + This will allow you to delete individual OIDC credentials for users even if + they have several set up. + +- Clarify session extend behavior + ([#3962](https://github.com/ory/kratos/issues/3962)) + ([af5ea35](https://github.com/ory/kratos/commit/af5ea35759e74d7a1637823abcc21dc8e3e39a9d)) +- Improve session extend performance + ([#3948](https://github.com/ory/kratos/issues/3948)) + ([4e3fad4](https://github.com/ory/kratos/commit/4e3fad4b4739b5cf00d658155350cb599f2cd06a)): + + This patch improves the performance for extending session lifespans. Lifespan + extension is tricky as it is often part of the middleware of Ory Kratos + consumers. As such, it is prone to transaction contention when we read and + write to the same session row at the same time (and potentially multiple + times). + + To address this, we: + + 1. Introduce a locking mechanism on the row to reduce transaction contention; + 2. Add a new feature flag that toggles returning 204 no content instead of + 200 + session. + + Be aware that all reads on the session table will have to wait for the + transaction to commit before they return a value. This may cause long(er) + response times on `/session/whoami` for sessions that are being extended at + the same time. + +- Password migration hook ([#3978](https://github.com/ory/kratos/issues/3978)) + ([c9d5573](https://github.com/ory/kratos/commit/c9d55730a10b71ac61bb5097f5f9c33f144f2a95)): + + This adds a password migration hook to easily migrate passwords for which we + do not have the hash. + + For each user that needs to be migrated to Ory Network, a new identity is + created with a credential of type password with a config of + {"use_password_migration_hook": true} . When a user logs in, the credential + identifier and password will be sent to the password_migration web hook if all + of these are true: The user’s identity’s password credential is + {"use_password_migration_hook": true} The password_migration hook is + configured After calling the password_migration hook, the HTTP status code + will be inspected: On 200, we parse the response as JSON and look for + {"status": "password_match"}. The password credential config will be replaced + with the hash of the actual password. On any other status code, we assume that + the password is not valid. + +### Tests + +- Deflake and parallelize persister tests + ([#3953](https://github.com/ory/kratos/issues/3953)) + ([61f87d9](https://github.com/ory/kratos/commit/61f87d90bd67e5bb1f00ee110d986e4f72fc4c91)) +- Deflake session extend config side-effect + ([#3950](https://github.com/ory/kratos/issues/3950)) + ([b192c92](https://github.com/ory/kratos/commit/b192c92d6c969d470d6479bc33dbc351d327c1f9)) +- Enable server-side config from context + ([#3954](https://github.com/ory/kratos/issues/3954)) + ([e0001b0](https://github.com/ory/kratos/commit/e0001b0db784457652581366bd7ead7cdf6b3898)) + +# [1.2.0](https://github.com/ory/kratos/compare/v1.1.0...v1.2.0) (2024-06-05) + +Ory Kratos v1.2 is the most complete, scalable, and secure open-source identity +server available. We are thrilled to announce its release! + +![Ory Kratos 1.2 released](https://www.ory.sh/images/newsletter/kratos-1.2.0/banner.png) + +This release introduces two major features: two-step registration and full +PassKey with resident key support. + +Passkeys provide a secure and convenient authentication method, eliminating the +need for passwords while ensuring strong security. With this release, we have +added support for resident keys, enabling offline authentication. Credential +discovery allows users to link existing passkeys to their Ory account +seamlessly. + +[Watch the PassKey demo video](https://github.com/aeneasr/web-next-deprecated/assets/3372410/e676c518-c82a-42a6-821e-28aecadb270c) + +Two-step registration improves the user experience by dividing the registration +process into two steps. Users first enter their identity traits, and then choose +a credential method for authentication, resulting in a streamlined process. This +feature is especially useful when enabling multiple authentication strategies, +as it eliminates the need to repeat identity traits for each strategy. + +![Two-Step Registration](https://ik.imagekit.io/launchnotes/production/tr:w-1640,c-at_max,f-auto/ngul9dzfjdt3pe8benegjjeeagi1) + +The 107 commits since v1.1 include several improvements: + +- **Webhooks** now carry session information if available. +- **Transient Payloads** are now available across all self-service flows. +- **Sign in with Twitter** is now available. +- **Sign in with LinkedIn** now includes an additional v2 provider compatible + with LinkedIn's new SSO API. +- **Two-Step Registration**: An improved registration experience that separates + entering profile information from choosing authentication methods. +- **User Credentials Meta-Information** can now be included on the list + endpoint. +- **Social Sign-In** is now resilient to double-submit issues common with + Facebook and Apple mobile login. + +**Two-Step Registration Enabled by Default**: This is now the default setting. +To disable, set `selfservice.flows.registration.enable_legacy_flow` to `true`. + +- Improved account linking and credential discovery during sign-up. +- The `return_to` parameter is now respected in OIDC API flows. +- Adjustments to database indices. +- Enhanced error messages for security violations. +- Improved SDK types. +- The `verification` and `verification_ui` hooks are now available in the login + flow. +- Webhooks now contain the correct identity state in the after-verification hook + chain. + +We are doing this survey to find out how we can support self-hosted Ory users +better. We strive to provide you with the best product and service possible and +your feedback will help us understand what we're doing well and where we can +improve to better meet your needs. We truly value your opinion and thank you in +advance for taking the time to share your thoughts with us! + +Fill out the +[survey now](https://share-eu1.hsforms.com/15DiCnJpcRuijnpAdnDhxxwextgn)! ## Breaking Changes This feature enables two-step registration per default. Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To disable two-step registration, set -`selfservice.flows.registration.enable_legacy_one_step` to `true`. This value +`selfservice.flows.registration.enable_legacy_flow` to `true`. This value defaults to `false`. ### Bug Fixes @@ -362,8 +523,13 @@ defaults to `false`. - Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) +- Change return urls in quickstarts + ([#3928](https://github.com/ory/kratos/issues/3928)) + ([9730e09](https://github.com/ory/kratos/commit/9730e099a656d211389d8e993c64d8082784c929)) - Close res body ([#3870](https://github.com/ory/kratos/issues/3870)) ([cc39f8d](https://github.com/ory/kratos/commit/cc39f8df7c235af0df616432bc4f88681896ad85)) +- CVEs in dependencies ([#3902](https://github.com/ory/kratos/issues/3902)) + ([e5d3b0a](https://github.com/ory/kratos/commit/e5d3b0afde3c80c6c9cf8815c56d82e291ede663)) - Db index and duplicate credentials error ([#3896](https://github.com/ory/kratos/issues/3896)) ([9f34a21](https://github.com/ory/kratos/commit/9f34a21ea2035a5d33edd96753023a3c8c6c054c)): @@ -411,6 +577,9 @@ defaults to `false`. - Missing indices and foreign keys ([#3800](https://github.com/ory/kratos/issues/3800)) ([0b32ce1](https://github.com/ory/kratos/commit/0b32ce113be47aa724d3468062ced09f8f60c52a)) +- **oidc:** Grace period for continuity container on oidc callbacks + ([#3915](https://github.com/ory/kratos/issues/3915)) + ([1a9a096](https://github.com/ory/kratos/commit/1a9a096d619925dd3718ad9dd9daf77387572ece)) - Passing transient payloads ([#3838](https://github.com/ory/kratos/issues/3838)) ([d01b670](https://github.com/ory/kratos/commit/d01b6705bf36efb6e0f3d71ed22d0574ab8a98a4)) @@ -473,6 +642,17 @@ defaults to `false`. - fix: transient payload with OIDC login +### Code Generation + +- Pin v1.2.0 release commit + ([1a70648](https://github.com/ory/kratos/commit/1a70648c4d5b9b8d135dd7bea3842057e67b574e)) + +### Documentation + +- Remove delete reference from batch patch identity + ([#3906](https://github.com/ory/kratos/issues/3906)) + ([cd01cb9](https://github.com/ory/kratos/commit/cd01cb9fb23a24e52d46538a9ea63c2144c3b145)) + ### Features - Add `include_credential` query param to `/admin/identities` list call @@ -491,6 +671,9 @@ defaults to `false`. - Add verification hook to login flow ([#3829](https://github.com/ory/kratos/issues/3829)) ([43e4ead](https://github.com/ory/kratos/commit/43e4eadce7fa6e66bf1f9c03136d141bffd3094f)) +- Allow admin to create API code recovery flows + ([#3939](https://github.com/ory/kratos/issues/3939)) + ([25d1ecd](https://github.com/ory/kratos/commit/25d1ecd90317193095e01b97ff21d92920035b02)) - Control edge cache ttl ([#3808](https://github.com/ory/kratos/issues/3808)) ([c9dcce5](https://github.com/ory/kratos/commit/c9dcce5a41137937df1aad7ac81170b443740f88)) - Linkedin v2 provider ([#3804](https://github.com/ory/kratos/issues/3804)) @@ -520,6 +703,22 @@ defaults to `false`. - Resolve failing test for empty tokens ([#3775](https://github.com/ory/kratos/issues/3775)) ([7277368](https://github.com/ory/kratos/commit/7277368bc28df8f0badffc7e739cef20f05e9a02)) +- Resolve flaky e2e tests ([#3935](https://github.com/ory/kratos/issues/3935)) + ([a14927d](https://github.com/ory/kratos/commit/a14927dfa5f8d0fbda7e5a831f0a09a42369e06c)): + + - test: resolve flaky code registration tests + + - chore: don't fail logout if cookie is not found + + - chore: remove .only + + - chore: reduce wait + + - chore: u + + - chore: u + + - chore: u ### Unclassified From a84fb3feab627a99d75f10a20230b87842463ed0 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Fri, 5 Jul 2024 11:28:55 +0200 Subject: [PATCH 133/262] chore: improve courier logging (#3985) --- courier/smtp_channel.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/courier/smtp_channel.go b/courier/smtp_channel.go index 9ed9335f8e7f..15a685bcd7ad 100644 --- a/courier/smtp_channel.go +++ b/courier/smtp_channel.go @@ -107,8 +107,8 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { gm.AddAlternative("text/html", htmlBody) } - if err := c.smtpClient.DialAndSend(ctx, gm); err != nil { - c.d.Logger(). + if err := errors.WithStack(c.smtpClient.DialAndSend(ctx, gm)); err != nil { + logger. WithError(err). Error("Unable to send email using SMTP connection.") From 7e7fdc26467e456674f5eca011187fc213bb833f Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:18:50 +0000 Subject: [PATCH 134/262] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41491e3bebf5..10b384631715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-07-04)](#2024-07-04) +- [ (2024-07-05)](#2024-07-05) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Documentation](#documentation) @@ -330,7 +330,7 @@ -# [](https://github.com/ory/kratos/compare/v1.2.0...v) (2024-07-04) +# [](https://github.com/ory/kratos/compare/v1.2.0...v) (2024-07-05) ## Breaking Changes From b5a66e0dde3a8fa6fdeb727482481b6302589631 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Fri, 5 Jul 2024 12:50:07 +0200 Subject: [PATCH 135/262] fix: move password migration hook config (#3986) This moves the password migration hook to ```yaml selfservice: methods: password: config: migrate_hook: ... ``` --- driver/config/config.go | 2 +- driver/config/config_test.go | 2 +- embedx/config.schema.json | 110 +++++++++++++++++------------------ 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index ac394d7c8518..81be527a2632 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -203,7 +203,7 @@ const ( ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls" ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level" ViperKeyVersion = "version" - ViperKeyPasswordMigrationHook = "selfservice.flows.login.password_migration" + ViperKeyPasswordMigrationHook = "selfservice.methods.password.config.migrate_hook" ) const ( diff --git a/driver/config/config_test.go b/driver/config/config_test.go index dc276eb3a171..8f9dfaaf20ec 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -218,7 +218,7 @@ func TestViperProvider(t *testing.T) { config string enabled bool }{ - {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"min_password_length":8,"identifier_similarity_check_enabled":true}`}, + {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"migrate_hook":{"config":{"emit_analytics_event":true,"method":"POST"},"enabled":false},"min_password_length":8,"identifier_similarity_check_enabled":true}`}, {id: "oidc", enabled: true, config: `{"providers":[{"client_id":"a","client_secret":"b","id":"github","provider":"github","mapper_url":"http://test.kratos.ory.sh/default-identity.schema.json"}]}`}, {id: "totp", enabled: true, config: `{"issuer":"issuer.ory.sh"}`}, } { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index e763c402a91a..c62b3c39f00c 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1303,61 +1303,6 @@ "enum": ["one_step", "identifier_first"], "default": "one_step" }, - "password_migration": { - "type": "object", - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean", - "title": "Enable Password Migration", - "description": "If set to true will enable password migration.", - "default": false - }, - "config": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "string", - "description": "The URL the password migration hook should call", - "format": "uri" - }, - "method": { - "type": "string", - "description": "The HTTP method to use (GET, POST, etc).", - "const": "POST", - "default": "POST" - }, - "headers": { - "type": "object", - "description": "The HTTP headers that must be applied to the password migration hook.", - "additionalProperties": { - "type": "string" - } - }, - "emit_analytics_event": { - "type": "boolean", - "default": true, - "description": "Emit tracing events for this hook on delivery or error" - }, - "auth": { - "type": "object", - "title": "Auth mechanisms", - "description": "Define which auth mechanism the Web-Hook should use", - "oneOf": [ - { - "$ref": "#/definitions/webHookAuthApiKeyProperties" - }, - { - "$ref": "#/definitions/webHookAuthBasicAuthProperties" - } - ] - }, - "additionalProperties": false - } - } - } - }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" }, @@ -1691,6 +1636,61 @@ "description": "If set to false the password validation does not check for similarity between the password and the user identifier.", "type": "boolean", "default": true + }, + "migrate_hook": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Password Migration", + "description": "If set to true will enable password migration.", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The URL the password migration hook should call", + "format": "uri" + }, + "method": { + "type": "string", + "description": "The HTTP method to use (GET, POST, etc).", + "const": "POST", + "default": "POST" + }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the password migration hook.", + "additionalProperties": { + "type": "string" + } + }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this hook on delivery or error" + }, + "auth": { + "type": "object", + "title": "Auth mechanisms", + "description": "Define which auth mechanism the Web-Hook should use", + "oneOf": [ + { + "$ref": "#/definitions/webHookAuthApiKeyProperties" + }, + { + "$ref": "#/definitions/webHookAuthBasicAuthProperties" + } + ] + }, + "additionalProperties": false + } + } + } } }, "additionalProperties": false From 1bdc19ae3e1a3df38234cb892f65de4a2c95f041 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 7 May 2024 10:31:14 +0200 Subject: [PATCH 136/262] feat: identifier first auth --- courier/sms_test.go | 3 +- driver/config/config.go | 14 + driver/registry_default.go | 2 + embedx/config.schema.json | 21 +- identity/credentials.go | 2 + identity/credentials_oidc.go | 2 +- internal/driver.go | 23 +- schema/errors.go | 11 +- selfservice/flow/login/error_test.go | 10 +- selfservice/flow/login/flow.go | 4 +- selfservice/flow/login/handler.go | 32 +- selfservice/flow/login/strategy.go | 1 - .../flow/login/strategy_form_hydrator.go | 38 +++ .../flow/login/strategy_form_hydrator_test.go | 36 +++ selfservice/flow/state.go | 9 +- selfservice/flowhelpers/login.go | 4 +- selfservice/strategy/code/strategy.go | 14 +- selfservice/strategy/code/strategy_login.go | 25 ++ .../multistep/.schema/login.schema.json | 24 ++ selfservice/strategy/multistep/schema.go | 11 + selfservice/strategy/multistep/strategy.go | 63 ++++ .../strategy/multistep/strategy_login.go | 161 ++++++++++ selfservice/strategy/multistep/types.go | 26 ++ selfservice/strategy/oidc/strategy.go | 35 +- selfservice/strategy/oidc/strategy_login.go | 53 +++ selfservice/strategy/passkey/passkey_login.go | 302 ++++++++++-------- .../strategy/passkey/passkey_login_test.go | 4 +- .../strategy/passkey/passkey_strategy.go | 1 - selfservice/strategy/password/login.go | 86 +++-- selfservice/strategy/webauthn/login.go | 184 ++++++----- selfservice/strategy/webauthn/strategy.go | 1 - test/e2e/cypress/support/commands.ts | 2 +- test/e2e/profiles/code/.kratos.yml | 3 +- test/e2e/profiles/email/.kratos.yml | 2 + test/e2e/profiles/mfa/.kratos.yml | 2 + test/e2e/profiles/mobile/.kratos.yml | 3 + test/e2e/profiles/network/.kratos.yml | 2 + .../profiles/oidc-provider-mfa/.kratos.yml | 2 + test/e2e/profiles/oidc-provider/.kratos.yml | 2 + test/e2e/profiles/oidc/.kratos.yml | 2 + test/e2e/profiles/passkey/.kratos.yml | 2 + test/e2e/profiles/passwordless/.kratos.yml | 2 + test/e2e/profiles/recovery-mfa/.kratos.yml | 2 + test/e2e/profiles/recovery/.kratos.yml | 2 + test/e2e/profiles/spa/.kratos.yml | 2 + test/e2e/profiles/two-steps/.kratos.yml | 3 +- test/e2e/profiles/verification/.kratos.yml | 2 + test/e2e/profiles/webhooks/.kratos.yml | 2 + text/id.go | 32 +- text/message_login.go | 14 +- text/message_validation.go | 8 + ui/node/attributes.go | 101 +++++- ui/node/attributes_test.go | 64 ++++ ui/node/node.go | 33 ++ ui/node/node_test.go | 62 ++++ 55 files changed, 1229 insertions(+), 324 deletions(-) create mode 100644 selfservice/flow/login/strategy_form_hydrator.go create mode 100644 selfservice/flow/login/strategy_form_hydrator_test.go create mode 100644 selfservice/strategy/multistep/.schema/login.schema.json create mode 100644 selfservice/strategy/multistep/schema.go create mode 100644 selfservice/strategy/multistep/strategy.go create mode 100644 selfservice/strategy/multistep/strategy_login.go create mode 100644 selfservice/strategy/multistep/types.go diff --git a/courier/sms_test.go b/courier/sms_test.go index a93a7974bf71..5dc727048be6 100644 --- a/courier/sms_test.go +++ b/courier/sms_test.go @@ -63,6 +63,7 @@ func TestQueueSMS(t *testing.T) { Body: body.Body, }) })) + t.Cleanup(srv.Close) requestConfig := fmt.Sprintf(`{ "url": "%s", @@ -112,8 +113,6 @@ func TestQueueSMS(t *testing.T) { assert.Equal(t, expected.To, message.To) assert.Equal(t, fmt.Sprintf("stub sms body %s\n", expected.Body), message.Body) } - - srv.Close() } func TestDisallowedInternalNetwork(t *testing.T) { diff --git a/driver/config/config.go b/driver/config/config.go index 81be527a2632..d9615451777e 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -134,6 +134,8 @@ const ( ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after" ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.flows.registration.before.hooks" ViperKeySelfServiceLoginUI = "selfservice.flows.login.ui_url" + ViperKeySelfServiceLoginFlowTwoStepEnabled = "selfservice.flows.login.two_step.enabled" + ViperKeySecurityAccountEnumerationMitigate = "security.account_enumeration.mitigate" ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan" ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after" ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks" @@ -782,8 +784,12 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self // we need to forcibly set these values here: defaultEnabled := false switch strategy { + case "identity_discovery": + defaultEnabled = p.SelfServiceLoginFlowTwoStepEnabled(ctx) + break case "code", "password", "profile": defaultEnabled = true + break } // Backwards compatibility for the old "passwordless_enabled" key @@ -1612,3 +1618,11 @@ func (p *Config) PasswordMigrationHook(ctx context.Context) (hook *PasswordMigra return hook } + +func (p *Config) SelfServiceLoginFlowTwoStepEnabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySelfServiceLoginFlowTwoStepEnabled) +} + +func (p *Config) SecurityAccountEnumerationMitigate(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySecurityAccountEnumerationMitigate) +} diff --git a/driver/registry_default.go b/driver/registry_default.go index eab63a120981..0c5f59238d28 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -6,6 +6,7 @@ package driver import ( "context" "crypto/sha256" + "github.com/ory/kratos/selfservice/strategy/multistep" "net/http" "strings" "sync" @@ -324,6 +325,7 @@ func (m *RegistryDefault) selfServiceStrategies() []any { passkey.NewStrategy(m), webauthn.NewStrategy(m), lookup.NewStrategy(m), + multistep.NewStrategy(m), } } } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index c62b3c39f00c..5016926ab036 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1557,12 +1557,6 @@ "title": "Enables login flows code method to fulfil MFA requests", "default": false }, - "passwordless_login_fallback_enabled": { - "type": "boolean", - "title": "Passwordless Login Fallback Enabled", - "description": "This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.", - "default": false - }, "enabled": { "type": "boolean", "title": "Enables Code Method", @@ -2879,6 +2873,21 @@ } } }, + "security": { + "type": "object", + "properties": { + "account_enumeration": { + "type": "object", + "properties": { + "mitigate": { + "type": "boolean", + "default": false, + "description": "Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet." + } + } + } + } + }, "version": { "title": "The kratos version this config is written for.", "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", diff --git a/identity/credentials.go b/identity/credentials.go index 3cc910c5a74e..3c7fa0f31834 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -88,6 +88,8 @@ const ( CredentialsTypeCodeAuth CredentialsType = "code" CredentialsTypePasskey CredentialsType = "passkey" CredentialsTypeProfile CredentialsType = "profile" + + TwoStep CredentialsType = "identity_discovery" // TODO move this somewhere else ) func (c CredentialsType) String() string { diff --git a/identity/credentials_oidc.go b/identity/credentials_oidc.go index 27462f927024..bcd03673c616 100644 --- a/identity/credentials_oidc.go +++ b/identity/credentials_oidc.go @@ -88,7 +88,7 @@ func NewCredentialsOIDC(tokens *CredentialsOIDCEncryptedTokens, provider, subjec return &Credentials{ Type: CredentialsTypeOIDC, - Identifiers: []string{OIDCUniqueID(provider, subject)}, + Identifiers: []string{OIDCUniqueID(provider, subject) /* getEmailFromTraits (needs to be verified) */}, Config: b.Bytes(), }, nil } diff --git a/internal/driver.go b/internal/driver.go index b95b2dc7c0a9..c10134347bd3 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -42,17 +42,18 @@ func init() { func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) *config.Config { configOpts := append([]configx.OptionModifier{ configx.WithValues(map[string]interface{}{ - "log.level": "error", - config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), - config.ViperKeyHasherArgon2ConfigMemory: 16384, - config.ViperKeyHasherArgon2ConfigIterations: 1, - config.ViperKeyHasherArgon2ConfigParallelism: 1, - config.ViperKeyHasherArgon2ConfigSaltLength: 16, - config.ViperKeyHasherBcryptCost: 4, - config.ViperKeyHasherArgon2ConfigKeyLength: 16, - config.ViperKeyCourierSMTPURL: "smtp://foo:bar@baz.com/", - config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.sh/redirect-not-set", - config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, + "log.level": "error", + config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), + config.ViperKeyHasherArgon2ConfigMemory: 16384, + config.ViperKeyHasherArgon2ConfigIterations: 1, + config.ViperKeyHasherArgon2ConfigParallelism: 1, + config.ViperKeyHasherArgon2ConfigSaltLength: 16, + config.ViperKeyHasherBcryptCost: 4, + config.ViperKeyHasherArgon2ConfigKeyLength: 16, + config.ViperKeyCourierSMTPURL: "smtp://foo:bar@baz.com/", + config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.sh/redirect-not-set", + config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, + config.ViperKeySelfServiceLoginFlowTwoStepEnabled: false, }), configx.SkipValidation(), }, opts...) diff --git a/schema/errors.go b/schema/errors.go index 30c1f72976f3..6ff52a5047c2 100644 --- a/schema/errors.go +++ b/schema/errors.go @@ -117,12 +117,21 @@ func NewInvalidCredentialsError() error { ValidationError: &jsonschema.ValidationError{ Message: `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`, InstancePtr: "#/", - Context: &ValidationErrorContextPasswordPolicyViolation{}, }, Messages: new(text.Messages).Add(text.NewErrorValidationInvalidCredentials()), }) } +func NewAccountNotFoundError() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: "this account does not exist or has no login method configured", + InstancePtr: "#/identifier", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationAccountNotFound()), + }) +} + type ValidationErrorContextDuplicateCredentialsError struct { AvailableCredentials []string `json:"available_credential_types"` AvailableOIDCProviders []string `json:"available_oidc_providers"` diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go index 5cc78c35bda1..e6f27f452225 100644 --- a/selfservice/flow/login/error_test.go +++ b/selfservice/flow/login/error_test.go @@ -6,13 +6,12 @@ package login_test import ( "context" "encoding/json" + "github.com/ory/kratos/identity" "io" "net/http" "testing" "time" - "github.com/ory/kratos/identity" - "github.com/gofrs/uuid" "github.com/ory/kratos/ui/node" @@ -74,7 +73,12 @@ func TestHandleError(t *testing.T) { require.NoError(t, err) for _, s := range reg.LoginStrategies(context.Background()) { - require.NoError(t, s.PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f)) + switch s.(type) { + case login.LegacyFormHydrator: + require.NoError(t, s.(login.LegacyFormHydrator).PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f)) + case login.FormHydrator: + require.NoError(t, s.(login.FormHydrator).PopulateLoginMethodFirstFactor(req, f)) + } } require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), f)) diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index a01d449a2751..06b189d6c351 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -230,9 +230,9 @@ func (f Flow) GetID() uuid.UUID { return f.ID } -// IsForced returns true if the login flow was triggered to re-authenticate the user. +// IsRefresh returns true if the login flow was triggered to re-authenticate the user. // This is the case if the refresh query parameter is set to true. -func (f *Flow) IsForced() bool { +func (f *Flow) IsRefresh() bool { return f.Refresh } diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index 88b3712602a0..bf53f7591115 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -212,8 +212,36 @@ preLoginHook: } for _, s := range h.d.LoginStrategies(r.Context(), strategyFilters...) { - if err := s.PopulateLoginMethod(r, f.RequestedAAL, f); err != nil { - return nil, nil, err + var populateErr error + + switch strat := s.(type) { + case FormHydrator: + switch { + case f.IsRefresh(): + populateErr = strat.PopulateLoginMethodRefresh(r, f) + break + case f.RequestedAAL == identity.AuthenticatorAssuranceLevel1: + if h.d.Config().SelfServiceLoginFlowTwoStepEnabled(r.Context()) { + populateErr = strat.PopulateLoginMethodMultiStepIdentification(r, f) + } else { + populateErr = strat.PopulateLoginMethodFirstFactor(r, f) + } + break + case f.RequestedAAL == identity.AuthenticatorAssuranceLevel2: + populateErr = strat.PopulateLoginMethodSecondFactor(r, f) + break + } + break + case LegacyFormHydrator: + populateErr = strat.PopulateLoginMethod(r, f.RequestedAAL, f) + break + default: + populateErr = errors.WithStack(x.PseudoPanic.WithReasonf("A login strategy was expected to implement one of the interfaces LegacyFormHydrator or FormHydrator but did not.")) + break + } + + if populateErr != nil { + return nil, nil, populateErr } } diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go index c70ad9cc8684..fec71d3beb1d 100644 --- a/selfservice/flow/login/strategy.go +++ b/selfservice/flow/login/strategy.go @@ -20,7 +20,6 @@ type Strategy interface { ID() identity.CredentialsType NodeGroup() node.UiNodeGroup RegisterLoginRoutes(*x.RouterPublic) - PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error Login(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (i *identity.Identity, err error) CompletedAuthenticationMethod(ctx context.Context, methods session.AuthenticationMethods) session.AuthenticationMethod } diff --git a/selfservice/flow/login/strategy_form_hydrator.go b/selfservice/flow/login/strategy_form_hydrator.go new file mode 100644 index 000000000000..1f87aeabf1b8 --- /dev/null +++ b/selfservice/flow/login/strategy_form_hydrator.go @@ -0,0 +1,38 @@ +package login + +import ( + "github.com/ory/kratos/identity" + "net/http" +) + +type LegacyFormHydrator interface { + PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error +} + +type FormHydrator interface { + PopulateLoginMethodRefresh(r *http.Request, sr *Flow) error + PopulateLoginMethodFirstFactor(r *http.Request, sr *Flow) error + PopulateLoginMethodSecondFactor(r *http.Request, sr *Flow) error + PopulateLoginMethodMultiStepSelection(r *http.Request, sr *Flow, options ...FormHydratorModifier) error + PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *Flow) error +} + +type FormHydratorOptions struct { + IdentityHint *identity.Identity +} + +type FormHydratorModifier func(o *FormHydratorOptions) + +func WithIdentityHint(i *identity.Identity) FormHydratorModifier { + return func(o *FormHydratorOptions) { + o.IdentityHint = i + } +} + +func NewFormHydratorOptions(modifiers []FormHydratorModifier) *FormHydratorOptions { + o := new(FormHydratorOptions) + for _, m := range modifiers { + m(o) + } + return o +} diff --git a/selfservice/flow/login/strategy_form_hydrator_test.go b/selfservice/flow/login/strategy_form_hydrator_test.go new file mode 100644 index 000000000000..db63de83bf35 --- /dev/null +++ b/selfservice/flow/login/strategy_form_hydrator_test.go @@ -0,0 +1,36 @@ +package login + +import ( + "github.com/ory/kratos/identity" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestWithIdentityHint(t *testing.T) { + expected := new(identity.Identity) + opts := NewFormHydratorOptions([]FormHydratorModifier{WithIdentityHint(expected)}) + assert.Equal(t, expected, opts.IdentityHint) +} + +func TestWithAccountEnumerationBucket(t *testing.T) { + opts := NewFormHydratorOptions([]FormHydratorModifier{}) + for _, c := range identity.AllCredentialTypes { + assert.Falsef(t, opts.BucketShowsCredential(c), "expected false for %s", c) + } + + opts = NewFormHydratorOptions([]FormHydratorModifier{WithAccountEnumerationBucket("hello@ory.sh")}) + found := 0 + var foundType identity.CredentialsType + for _, c := range identity.AllCredentialTypes { + c := c + if opts.BucketShowsCredential(c) { + foundType = c + found++ + } + } + + assert.Equal(t, 1, found, "expected exactly one to be true") + + opts = NewFormHydratorOptions([]FormHydratorModifier{WithAccountEnumerationBucket("hello@ory.sh")}) + assert.Truef(t, opts.BucketShowsCredential(foundType), "expected true for %s because bucket should be stable", foundType) +} diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go index 76a0683fc19d..5ff346931f46 100644 --- a/selfservice/flow/state.go +++ b/selfservice/flow/state.go @@ -31,9 +31,16 @@ const ( StatePassedChallenge State = "passed_challenge" StateShowForm State = "show_form" StateSuccess State = "success" + + StateLoginIdentifierFirstForm State = "identifier_first_form" ) -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} +var states = []State{ + StateLoginIdentifierFirstForm, + StateChooseMethod, + StateEmailSent, + StatePassedChallenge, +} func indexOf(current State) int { for k, s := range states { diff --git a/selfservice/flowhelpers/login.go b/selfservice/flowhelpers/login.go index 2e97f85ebe30..60c17176a740 100644 --- a/selfservice/flowhelpers/login.go +++ b/selfservice/flowhelpers/login.go @@ -15,11 +15,11 @@ func GuessForcedLoginIdentifier(r *http.Request, d interface { session.ManagementProvider identity.PrivilegedPoolProvider }, f interface { - IsForced() bool + IsRefresh() bool }, ct identity.CredentialsType) (identifier string, id *identity.Identity, creds *identity.Credentials) { var ok bool // This block adds the identifier to the method when the request is forced - as a hint for the user. - if !f.IsForced() { + if !f.IsRefresh() { // do nothing } else if sess, err := d.SessionManager().FetchFromRequest(r.Context(), r); err != nil { // do nothing diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index ee3ce353e4ae..80147786477a 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -180,6 +180,7 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { if f.GetType() == flow.TypeBrowser { f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r)) } + return nil } @@ -299,15 +300,10 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error // preserve the login identifier that was submitted // so we can retry the code flow with the same data for _, n := range f.GetUI().Nodes { - if n.Group == node.DefaultGroup { - // we don't need the user to change the values here - // for better UX let's make them disabled - // when there are errors we won't hide the fields - if len(n.Messages) == 0 { - if input, ok := n.Attributes.(*node.InputAttributes); ok { - input.Type = "hidden" - n.Attributes = input - } + if n.ID() == "identifier" { + if input, ok := n.Attributes.(*node.InputAttributes); ok { + input.Type = "hidden" + n.Attributes = input } freshNodes = append(freshNodes, n) } diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index a9d7459f5c56..6054580cfd19 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -7,6 +7,7 @@ import ( "context" "database/sql" "encoding/json" + "github.com/ory/kratos/text" "net/http" "strings" @@ -29,6 +30,7 @@ import ( "github.com/ory/x/decoderx" ) +var _ login.FormHydrator = new(Strategy) var _ login.Strategy = new(Strategy) // Update Login flow using the code method @@ -372,3 +374,26 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi return i, nil } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(_ *http.Request, f *login.Flow, _ ...login.FormHydratorModifier) error { + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginCode()), + ) + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, f *login.Flow) error { + return nil +} diff --git a/selfservice/strategy/multistep/.schema/login.schema.json b/selfservice/strategy/multistep/.schema/login.schema.json new file mode 100644 index 000000000000..c19b425f41ea --- /dev/null +++ b/selfservice/strategy/multistep/.schema/login.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/identity_disovery/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "identifier": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string", + "enum": [ + "identity_discovery" + ] + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/multistep/schema.go b/selfservice/strategy/multistep/schema.go new file mode 100644 index 000000000000..53704e7cce72 --- /dev/null +++ b/selfservice/strategy/multistep/schema.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package multistep + +import ( + _ "embed" +) + +//go:embed .schema/login.schema.json +var loginSchema []byte diff --git a/selfservice/strategy/multistep/strategy.go b/selfservice/strategy/multistep/strategy.go new file mode 100644 index 000000000000..9d75de3a2e5b --- /dev/null +++ b/selfservice/strategy/multistep/strategy.go @@ -0,0 +1,63 @@ +package multistep + +import ( + "context" + "github.com/go-playground/validator/v10" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +type dependencies interface { + x.LoggingProvider + x.WriterProvider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + + config.Provider + + identity.PrivilegedPoolProvider + login.StrategyProvider + login.FlowPersistenceProvider +} + +type Strategy struct { + d dependencies + v *validator.Validate + hd *decoderx.HTTP +} + +func NewStrategy(d any) *Strategy { + return &Strategy{ + d: d.(dependencies), + v: validator.New(), + hd: decoderx.NewHTTP(), + } +} + +func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.TwoStep +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel1, + } +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.TwoStepGroup +} diff --git a/selfservice/strategy/multistep/strategy_login.go b/selfservice/strategy/multistep/strategy_login.go new file mode 100644 index 000000000000..3552c5f11095 --- /dev/null +++ b/selfservice/strategy/multistep/strategy_login.go @@ -0,0 +1,161 @@ +package multistep + +import ( + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "github.com/ory/x/sqlcon" + "github.com/pkg/errors" + "net/http" +) + +var _ login.FormHydrator = new(Strategy) +var _ login.Strategy = new(Strategy) + +func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithMultiStepMethod, err error) error { + if f != nil { + f.UI.Nodes.SetValueAttribute("identifier", payload.Identifier) + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (_ *identity.Identity, err error) { + if !s.d.Config().SelfServiceLoginFlowTwoStepEnabled(r.Context()) { + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, err + } + + var p updateLoginFlowWithMultiStepMethod + if err := s.hd.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + f.TransientPayload = p.TransientPayload + + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + + var opts []login.FormHydratorModifier + + // Look up the user by the identifier. + identityHint, err := s.d.PrivilegedIdentityPool().FindIdentityByCredentialIdentifier(r.Context(), p.Identifier, + // We are dealing with user input -> lookup should be case-insensitive. + false, + ) + if errors.Is(err, sqlcon.ErrNoRows) { + // User not found + if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + // We don't have to mitigate account enumeration and show the user that the account doesn't exist + return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewAccountNotFoundError())) + } + + // We have to mitigate account enumeration. So we continue without setting the identity hint. + } else if err != nil { + // An error happened during lookup + return nil, s.handleLoginError(w, r, f, &p, err) + } else if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + // Hydrate credentials + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(r.Context(), identityHint, identity.ExpandCredentials); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + } + + f.UI.ResetMessages() + f.UI.Nodes.SetValueAttribute("identifier", p.Identifier) + + // Add identity hint + opts = append(opts, login.WithIdentityHint(identityHint)) + + for _, ls := range s.d.LoginStrategies(r.Context()) { + populator, ok := ls.(login.FormHydrator) + if !ok { + continue + } + + if err := populator.PopulateLoginMethodMultiStepSelection(r, f, opts...); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + } + + f.Active = identity.TwoStep + if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + + if x.IsJSONRequest(r) { + s.d.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther) + } + + return nil, flow.ErrCompletedByStrategy +} + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, f *login.Flow) error { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + f.UI.SetNode(node.NewInputField("identifier", "", s.NodeGroup(), node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + f.UI.GetNodes().Append(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(_ *http.Request, f *login.Flow, _ ...login.FormHydratorModifier) error { + f.UI.GetNodes().RemoveMatching(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit)) + + // We set the identifier to hidden, so it's still available in the form but not visible to the user. + for k, n := range f.UI.Nodes { + if n.ID() != "identifier" { + continue + } + + attrs, ok := f.UI.Nodes[k].Attributes.(*node.InputAttributes) + if !ok { + continue + } + + attrs.Type = node.InputAttributeTypeHidden + f.UI.Nodes[k].Attributes = attrs + } + + return nil +} + +func (s *Strategy) RegisterLoginRoutes(_ *x.RouterPublic) {} diff --git a/selfservice/strategy/multistep/types.go b/selfservice/strategy/multistep/types.go new file mode 100644 index 000000000000..5268f9c49195 --- /dev/null +++ b/selfservice/strategy/multistep/types.go @@ -0,0 +1,26 @@ +package multistep + +import "encoding/json" + +// Update Login Flow with Multi-Step Method +// +// swagger:model updateLoginFlowWithMultiStepMethod +type updateLoginFlowWithMultiStepMethod struct { + // Method should be set to "password" when logging in using the identifier and password strategy. + // + // required: true + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // Identifier is the email or username of the user trying to log in. + // + // required: true + Identifier string `json:"identifier"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 6515d06367ee..2fe7b23265e3 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -24,7 +24,6 @@ import ( "golang.org/x/oauth2" "github.com/ory/kratos/cipher" - "github.com/ory/kratos/selfservice/flowhelpers" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/x/jsonnetsecure" "github.com/ory/x/otelx" @@ -537,38 +536,8 @@ func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(pro return err } - providers := conf.Providers - - if lf, ok := f.(*login.Flow); ok && lf.IsForced() { - if _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()); id != nil { - if c == nil { - // no OIDC credentials, don't add any providers - providers = nil - } else { - var credentials identity.CredentialsOIDC - if err := json.Unmarshal(c.Config, &credentials); err != nil { - // failed to read OIDC credentials, don't add any providers - providers = nil - } else { - // add only providers that can actually be used to log in as this identity - providers = make([]Configuration, 0, len(conf.Providers)) - for i := range conf.Providers { - for j := range credentials.Providers { - if conf.Providers[i].ID == credentials.Providers[j].Provider { - providers = append(providers, conf.Providers[i]) - break - } - } - } - } - } - } - } - - // does not need sorting because there is only one field - c := f.GetUI() - c.SetCSRF(s.d.GenerateCSRFToken(r)) - AddProviders(c, providers, message) + f.GetUI().SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(f.GetUI(), conf.Providers, message) return nil } diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 42b948ec7c11..43a697b961f2 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -6,6 +6,7 @@ package oidc import ( "bytes" "encoding/json" + "github.com/ory/kratos/selfservice/flowhelpers" "net/http" "strings" "time" @@ -34,6 +35,7 @@ import ( "github.com/ory/kratos/x" ) +var _ login.FormHydrator = new(Strategy) var _ login.Strategy = new(Strategy) func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { @@ -290,3 +292,54 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, errors.WithStack(flow.ErrCompletedByStrategy) } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, lf *login.Flow) error { + conf, err := s.Config(r.Context()) + if err != nil { + return err + } + + providers := conf.Providers + _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()) + if id == nil || c == nil { + providers = nil + } else { + var credentials identity.CredentialsOIDC + if err := json.Unmarshal(c.Config, &credentials); err != nil { + // failed to read OIDC credentials, don't add any providers + providers = nil + } else { + // add only providers that can actually be used to log in as this identity + providers = make([]Configuration, 0, len(conf.Providers)) + for i := range conf.Providers { + for j := range credentials.Providers { + if conf.Providers[i].ID == credentials.Providers[j].Provider { + providers = append(providers, conf.Providers[i]) + break + } + } + } + } + } + + lf.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(lf.UI, providers, text.NewInfoLoginWith) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + return s.populateMethod(r, f, text.NewInfoLoginWith) +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(_ *http.Request, sr *login.Flow, _ ...login.FormHydratorModifier) error { + sr.GetUI().UnsetNode("provider") + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, f *login.Flow) error { + return s.populateMethod(r, f, text.NewInfoLoginWith) +} diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index 63d5ec66f2f0..54b3f475ed38 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -29,23 +29,13 @@ import ( "github.com/ory/x/decoderx" ) +var _ login.FormHydrator = new(Strategy) + func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { webauthnx.RegisterWebauthnRoute(r) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, aal identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - if sr.Type != flow.TypeBrowser || aal != identity.AuthenticatorAssuranceLevel1 { - return nil - } - - return s.populateLoginMethodForPasskeys(r, sr) -} - func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *login.Flow) error { - if loginFlow.IsForced() { - return s.populateLoginMethodForRefresh(r, loginFlow) - } - ctx := r.Context() loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) @@ -113,119 +103,6 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo Type: node.InputAttributeTypeHidden, }}) - loginFlow.UI.Nodes.Append(node.NewInputField( - node.PasskeyLoginTrigger, - "", - node.PasskeyGroup, - node.InputAttributeTypeButton, - node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here - }), - ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) - - return nil -} - -func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *login.Flow) error { - ctx := r.Context() - - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, loginFlow, s.ID()) - if identifier == "" { - return nil - } - - id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) - if err != nil { - return err - } - - cred, ok := id.GetCredentials(s.ID()) - if !ok { - // Identity has no passkey - return nil - } - - var conf identity.CredentialsWebAuthnConfig - if err := json.Unmarshal(cred.Config, &conf); err != nil { - return errors.WithStack(err) - } - - webAuthCreds := conf.Credentials.ToWebAuthn() - if len(webAuthCreds) == 0 { - // Identity has no webauthn - return nil - } - - passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) - - webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) - if err != nil { - return errors.WithStack(err) - } - option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ - Name: passkeyIdentifier, - ID: conf.UserHandle, - Credentials: webAuthCreds, - Config: webAuthn.Config, - }) - if err != nil { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) - } - - loginFlow.InternalContext, err = sjson.SetBytes( - loginFlow.InternalContext, - flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), - sessionData, - ) - if err != nil { - return errors.WithStack(err) - } - - injectWebAuthnOptions, err := json.Marshal(option) - if err != nil { - return errors.WithStack(err) - } - - loginFlow.UI.Nodes.Upsert(&node.Node{ - Type: node.Input, - Group: node.PasskeyGroup, - Meta: &node.Meta{}, - Attributes: &node.InputAttributes{ - Name: node.PasskeyChallenge, - Type: node.InputAttributeTypeHidden, - FieldValue: string(injectWebAuthnOptions), - }}) - - loginFlow.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) - - loginFlow.UI.Nodes.Upsert(&node.Node{ - Type: node.Input, - Group: node.PasskeyGroup, - Meta: &node.Meta{}, - Attributes: &node.InputAttributes{ - Name: node.PasskeyLogin, - Type: node.InputAttributeTypeHidden, - }}) - - loginFlow.UI.Nodes.Append(node.NewInputField( - node.PasskeyLoginTrigger, - "", - node.PasskeyGroup, - node.InputAttributeTypeButton, - node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - }), - ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) - - loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - loginFlow.UI.SetNode(node.NewInputField( - "identifier", - passkeyIdentifier, - node.DefaultGroup, - node.InputAttributeTypeHidden, - )) - return nil } @@ -393,3 +270,178 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * return i, nil } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, f *login.Flow) error { + if f.Type != flow.TypeBrowser { + return nil + } + + ctx := r.Context() + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, f, s.ID()) + if identifier == "" { + return nil + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + if err != nil { + return err + } + + cred, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no passkey + return nil + } + + var conf identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return errors.WithStack(err) + } + + webAuthCreds := conf.Credentials.ToWebAuthn() + if len(webAuthCreds) == 0 { + // Identity has no webauthn + return nil + } + + passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ + Name: passkeyIdentifier, + ID: conf.UserHandle, + Credentials: webAuthCreds, + Config: webAuthn.Config, + }) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) + } + + f.InternalContext, err = sjson.SetBytes( + f.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + f.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + }}) + + f.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.SetNode(node.NewInputField( + "identifier", + passkeyIdentifier, + node.DefaultGroup, + node.InputAttributeTypeHidden, + )) + + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + if err := s.populateLoginMethodForPasskeys(r, sr); err != nil { + return err + } + + sr.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + o := login.NewFormHydratorOptions(opts) + + if o.IdentityHint == nil { + // Identity was not found so add fields + } else { + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + count, err := s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials) + if err != nil { + return err + } else if count == 0 && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return nil + } + } + + sr.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + return s.populateLoginMethodForPasskeys(r, sr) +} diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go index cae6aa0ee4dd..2a6c2075557e 100644 --- a/selfservice/strategy/passkey/passkey_login_test.go +++ b/selfservice/strategy/passkey/passkey_login_test.go @@ -45,12 +45,12 @@ func TestPopulateLoginMethod(t *testing.T) { t.Run("case=should not handle AAL2", func(t *testing.T) { loginFlow := &login.Flow{Type: flow.TypeBrowser} - assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel2, loginFlow)) + assert.Nil(t, s.PopulateLoginMethodSecondFactor(nil, loginFlow)) }) t.Run("case=should not handle API flows", func(t *testing.T) { loginFlow := &login.Flow{Type: flow.TypeAPI} - assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel1, loginFlow)) + assert.Nil(t, s.PopulateLoginMethodFirstFactor(nil, loginFlow)) }) } diff --git a/selfservice/strategy/passkey/passkey_strategy.go b/selfservice/strategy/passkey/passkey_strategy.go index b590a7e93b6d..25329f5781b1 100644 --- a/selfservice/strategy/passkey/passkey_strategy.go +++ b/selfservice/strategy/passkey/passkey_strategy.go @@ -6,7 +6,6 @@ package passkey import ( "context" "encoding/json" - "github.com/pkg/errors" "github.com/ory/kratos/continuity" diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 3600e29b4e0e..9381e50a9bb5 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -33,6 +33,8 @@ import ( "github.com/ory/kratos/x" ) +var _ login.FormHydrator = new(Strategy) + func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { } @@ -144,43 +146,79 @@ func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID return s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, i) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - // This strategy can only solve AAL1 - if requestedAAL > identity.AuthenticatorAssuranceLevel1 { +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, sr *login.Flow) error { + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) + if identifier == "" { return nil } - if sr.IsForced() { - // We only show this method on a refresh request if the user has indeed a password set. - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) - if identifier == "" { - return nil - } + // If we don't have a password set, do not show the password field. + count, err := s.CountActiveFirstFactorCredentials(id.Credentials) + if err != nil { + return err + } else if count == 0 { + return nil + } - count, err := s.CountActiveFirstFactorCredentials(id.Credentials) - if err != nil { - return err - } else if count == 0 { - return nil - } + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin())) + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) addIdentifierNode(r *http.Request, sr *login.Flow) error { + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if err := s.addIdentifierNode(r, sr); err != nil { + return err + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword())) + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + o := login.NewFormHydratorOptions(opts) + + if o.IdentityHint == nil { + // Identity was not found so add fields } else { - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + count, err := s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials) if err != nil { return err + } else if count == 0 && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return nil } - sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) - sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin())) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword())) + return nil +} +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *login.Flow) error { return nil } diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 4c7dd23f09ea..93cbe38fadd6 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -34,84 +34,14 @@ import ( "github.com/ory/x/decoderx" ) +var _ login.FormHydrator = new(Strategy) + func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { webauthnx.RegisterWebauthnRoute(r) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - if sr.Type != flow.TypeBrowser { - return nil - } - - if s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel1) { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - return nil - } else if sr.IsForced() { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - return nil - } else if !s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel2) { - // We have done proper validation before so this should never error - sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) - if err != nil { - return err - } - - if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - - return nil - } - - return nil -} - func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login.Flow) error { - if sr.IsForced() { - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) - if identifier == "" { - return nil - } - - if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) - return nil - } - - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) - if err != nil { - return err - } - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField( - "identifier", - "", - node.DefaultGroup, - node.InputAttributeTypeText, - node.WithRequiredInputAttribute, - func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, - ).WithMetaLabel(identifierLabel)) sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn())) return nil } @@ -134,7 +64,7 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident } webAuthCreds := conf.Credentials.ToWebAuthn() - if !sr.IsForced() { + if !sr.IsRefresh() { webAuthCreds = conf.Credentials.ToWebAuthnFiltered(aal) } @@ -245,7 +175,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(r, f, err) } - if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsForced() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { + if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsRefresh() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { return s.loginPasswordless(w, r, f, &p) } @@ -337,7 +267,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * } webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal) - if f.IsForced() { + if f.IsRefresh() { webAuthCreds = o.Credentials.ToWebAuthn() } @@ -365,3 +295,107 @@ func (s *Strategy) loginMultiFactor(w http.ResponseWriter, r *http.Request, f *l } return s.loginAuthenticate(w, r, f, identityID, p, identity.AuthenticatorAssuranceLevel2) } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) + if identifier == "" { + return nil + } + + if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField( + "identifier", + "", + node.DefaultGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute, + func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, + ).WithMetaLabel(identifierLabel)) + + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser || s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + + // We have done proper validation before so this should never error + sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) + if err != nil { + return err + } + + if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + if sr.Type != flow.TypeBrowser && !s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + + o := login.NewFormHydratorOptions(opts) + if o.IdentityHint == nil { + // Identity was not found so add fields + } else { + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + count, err := s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials) + if err != nil { + return err + } else if count == 0 && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return nil + } + } + + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + return nil + +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *login.Flow) error { + return nil +} diff --git a/selfservice/strategy/webauthn/strategy.go b/selfservice/strategy/webauthn/strategy.go index 998490055996..ba2ced37b3e5 100644 --- a/selfservice/strategy/webauthn/strategy.go +++ b/selfservice/strategy/webauthn/strategy.go @@ -6,7 +6,6 @@ package webauthn import ( "context" "encoding/json" - "github.com/pkg/errors" "github.com/ory/kratos/continuity" diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 0b4584646abc..199dfa81a79a 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -429,7 +429,7 @@ Cypress.Commands.add( f.group === "default" && "name" in f.attributes && f.attributes.name === "traits.email", - ).attributes.value, + )?.attributes.value, ).to.eq(email) return cy diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 3e98857e1628..680fb7255457 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -19,6 +19,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false after: code: hooks: @@ -38,7 +40,6 @@ selfservice: enabled: true code: passwordless_enabled: true - passwordless_login_fallback_enabled: false enabled: true config: lifespan: 1h diff --git a/test/e2e/profiles/email/.kratos.yml b/test/e2e/profiles/email/.kratos.yml index b1d62a3e25c4..dbc47fa538b7 100644 --- a/test/e2e/profiles/email/.kratos.yml +++ b/test/e2e/profiles/email/.kratos.yml @@ -18,6 +18,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/mfa/.kratos.yml b/test/e2e/profiles/mfa/.kratos.yml index 99becd59a868..d2fd33e1a13b 100644 --- a/test/e2e/profiles/mfa/.kratos.yml +++ b/test/e2e/profiles/mfa/.kratos.yml @@ -19,6 +19,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/mobile/.kratos.yml b/test/e2e/profiles/mobile/.kratos.yml index c0a46e57c197..32b01485c91b 100644 --- a/test/e2e/profiles/mobile/.kratos.yml +++ b/test/e2e/profiles/mobile/.kratos.yml @@ -20,6 +20,9 @@ selfservice: verification: enabled: false + login: + two_step: + enabled: false methods: totp: enabled: true diff --git a/test/e2e/profiles/network/.kratos.yml b/test/e2e/profiles/network/.kratos.yml index c3c8b3daedd7..41ba01f50303 100644 --- a/test/e2e/profiles/network/.kratos.yml +++ b/test/e2e/profiles/network/.kratos.yml @@ -21,6 +21,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false before: hooks: - hook: web_hook diff --git a/test/e2e/profiles/oidc-provider-mfa/.kratos.yml b/test/e2e/profiles/oidc-provider-mfa/.kratos.yml index ac577ce45724..690da6b70b3f 100644 --- a/test/e2e/profiles/oidc-provider-mfa/.kratos.yml +++ b/test/e2e/profiles/oidc-provider-mfa/.kratos.yml @@ -21,6 +21,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/oidc-provider/.kratos.yml b/test/e2e/profiles/oidc-provider/.kratos.yml index 09b2c9978700..900ebf1fb0e4 100644 --- a/test/e2e/profiles/oidc-provider/.kratos.yml +++ b/test/e2e/profiles/oidc-provider/.kratos.yml @@ -42,6 +42,8 @@ selfservice: - hook: session login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/oidc/.kratos.yml b/test/e2e/profiles/oidc/.kratos.yml index b0a327bb5096..f174237751cd 100644 --- a/test/e2e/profiles/oidc/.kratos.yml +++ b/test/e2e/profiles/oidc/.kratos.yml @@ -51,6 +51,8 @@ selfservice: - hook: session login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/passkey/.kratos.yml b/test/e2e/profiles/passkey/.kratos.yml index 85441f599e1b..0f08d87434d8 100644 --- a/test/e2e/profiles/passkey/.kratos.yml +++ b/test/e2e/profiles/passkey/.kratos.yml @@ -22,6 +22,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/passwordless/.kratos.yml b/test/e2e/profiles/passwordless/.kratos.yml index b3582a61216c..4fc40604a148 100644 --- a/test/e2e/profiles/passwordless/.kratos.yml +++ b/test/e2e/profiles/passwordless/.kratos.yml @@ -25,6 +25,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/recovery-mfa/.kratos.yml b/test/e2e/profiles/recovery-mfa/.kratos.yml index 03b0337cef2f..8e215ef0d162 100644 --- a/test/e2e/profiles/recovery-mfa/.kratos.yml +++ b/test/e2e/profiles/recovery-mfa/.kratos.yml @@ -22,6 +22,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false registration: ui_url: http://localhost:4455/registration error: diff --git a/test/e2e/profiles/recovery/.kratos.yml b/test/e2e/profiles/recovery/.kratos.yml index 3d3ca8f3aca7..00077bb140c3 100644 --- a/test/e2e/profiles/recovery/.kratos.yml +++ b/test/e2e/profiles/recovery/.kratos.yml @@ -21,6 +21,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false registration: ui_url: http://localhost:4455/registration error: diff --git a/test/e2e/profiles/spa/.kratos.yml b/test/e2e/profiles/spa/.kratos.yml index 6d5eb44a67de..69c169f7ccf0 100644 --- a/test/e2e/profiles/spa/.kratos.yml +++ b/test/e2e/profiles/spa/.kratos.yml @@ -23,6 +23,8 @@ selfservice: hook: session login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/two-steps/.kratos.yml b/test/e2e/profiles/two-steps/.kratos.yml index d23dd0bce07c..01b35e53d2cc 100644 --- a/test/e2e/profiles/two-steps/.kratos.yml +++ b/test/e2e/profiles/two-steps/.kratos.yml @@ -28,6 +28,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: @@ -66,7 +68,6 @@ selfservice: code: enabled: true passwordless_enabled: true - passwordless_login_fallback_enabled: false config: lifespan: 1h diff --git a/test/e2e/profiles/verification/.kratos.yml b/test/e2e/profiles/verification/.kratos.yml index ca4932f18c08..881f8d43a58e 100644 --- a/test/e2e/profiles/verification/.kratos.yml +++ b/test/e2e/profiles/verification/.kratos.yml @@ -26,6 +26,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false registration: ui_url: http://localhost:4455/registration error: diff --git a/test/e2e/profiles/webhooks/.kratos.yml b/test/e2e/profiles/webhooks/.kratos.yml index 00eb10537adb..f4f6698a3f29 100644 --- a/test/e2e/profiles/webhooks/.kratos.yml +++ b/test/e2e/profiles/webhooks/.kratos.yml @@ -30,6 +30,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false after: password: hooks: diff --git a/text/id.go b/text/id.go index a466caec0f8c..edff417a2738 100644 --- a/text/id.go +++ b/text/id.go @@ -31,6 +31,7 @@ const ( InfoSelfServiceLoginCodeMFA // 1010019 InfoSelfServiceLoginCodeMFAHint // 1010020 InfoSelfServiceLoginPasskey // 1010021 + InfoSelfServiceLoginPassword // 1010022 ) const ( @@ -86,21 +87,21 @@ const ( ) const ( - InfoNodeLabel ID = 1070000 + iota // 1070000 - InfoNodeLabelInputPassword // 1070001 - InfoNodeLabelGenerated // 1070002 - InfoNodeLabelSave // 1070003 - InfoNodeLabelID // 1070004 - InfoNodeLabelSubmit // 1070005 - InfoNodeLabelVerifyOTP // 1070006 - InfoNodeLabelEmail // 1070007 - InfoNodeLabelResendOTP // 1070008 - InfoNodeLabelContinue // 1070009 - InfoNodeLabelRecoveryCode // 1070010 - InfoNodeLabelVerificationCode // 1070011 - InfoNodeLabelRegistrationCode // 1070012 - InfoNodeLabelLoginCode // 1070013 - InfoNodeLabelLoginAndLinkCredential + InfoNodeLabel ID = 1070000 + iota // 1070000 + InfoNodeLabelInputPassword // 1070001 + InfoNodeLabelGenerated // 1070002 + InfoNodeLabelSave // 1070003 + InfoNodeLabelID // 1070004 + InfoNodeLabelSubmit // 1070005 + InfoNodeLabelVerifyOTP // 1070006 + InfoNodeLabelEmail // 1070007 + InfoNodeLabelResendOTP // 1070008 + InfoNodeLabelContinue // 1070009 + InfoNodeLabelRecoveryCode // 1070010 + InfoNodeLabelVerificationCode // 1070011 + InfoNodeLabelRegistrationCode // 1070012 + InfoNodeLabelLoginCode // 1070013 + InfoNodeLabelLoginAndLinkCredential // 1070014 ) const ( @@ -148,6 +149,7 @@ const ( ErrorValidationPasswordTooManyBreaches ErrorValidationNoCodeUser ErrorValidationTraitsMismatch + ErrorValidationAccountNotFound ) const ( diff --git a/text/message_login.go b/text/message_login.go index ec627458a028..9312a21e97a2 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -89,6 +89,14 @@ func NewInfoLoginTOTP() *Message { } } +func NewInfoLoginPassword() *Message { + return &Message{ + ID: InfoSelfServiceLoginPassword, + Text: "Sign in with password", + Type: Info, + } +} + func NewInfoLoginLookup() *Message { return &Message{ ID: InfoLoginLookup, @@ -182,7 +190,7 @@ func NewErrorValidationVerificationNoStrategyFound() *Message { func NewInfoSelfServiceLoginWebAuthn() *Message { return &Message{ ID: InfoSelfServiceLoginWebAuthn, - Text: "Use security key", + Text: "Sign in with hardware key", Type: Info, } } @@ -198,7 +206,7 @@ func NewInfoSelfServiceLoginPasskey() *Message { func NewInfoSelfServiceContinueLoginWebAuthn() *Message { return &Message{ ID: InfoSelfServiceLoginContinueWebAuthn, - Text: "Continue with security key", + Text: "Sign in with hardware key", Type: Info, } } @@ -239,7 +247,7 @@ func NewInfoSelfServiceLoginCode() *Message { return &Message{ ID: InfoSelfServiceLoginCode, Type: Info, - Text: "Sign in with code", + Text: "Send sign in code", } } diff --git a/text/message_validation.go b/text/message_validation.go index c10fddead805..2fd1e4c2d28d 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -257,6 +257,14 @@ func NewErrorValidationInvalidCredentials() *Message { } } +func NewErrorValidationAccountNotFound() *Message { + return &Message{ + ID: ErrorValidationAccountNotFound, + Text: "This account does not exist or has no login method configured.", + Type: Error, + } +} + func NewErrorValidationDuplicateCredentials() *Message { return &Message{ ID: ErrorValidationDuplicateCredentials, diff --git a/ui/node/attributes.go b/ui/node/attributes.go index 9611b5828dff..762df9fd46c7 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -3,7 +3,10 @@ package node -import "github.com/ory/kratos/text" +import ( + "fmt" + "github.com/ory/kratos/text" +) const ( InputAttributeTypeText UiNodeInputAttributeType = "text" @@ -53,6 +56,9 @@ type Attributes interface { // swagger:ignore GetNodeType() UiNodeType + + // swagger:ignore + Matches(other Attributes) bool } // InputAttributes represents the attributes of an input node @@ -267,6 +273,99 @@ func (a *ScriptAttributes) ID() string { return a.Identifier } +func (a *InputAttributes) Matches(other Attributes) bool { + ot, ok := other.(*InputAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.Type) > 0 && a.Type != ot.Type { + return false + } + + if ot.FieldValue != nil && fmt.Sprintf("%v", a.FieldValue) != fmt.Sprintf("%v", ot.FieldValue) { + return false + } + + if len(ot.Name) > 0 && a.Name != ot.Name { + return false + } + + return true +} + +func (a *ImageAttributes) Matches(other Attributes) bool { + ot, ok := other.(*ImageAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.Source) > 0 && a.Source != ot.Source { + return false + } + + return true +} + +func (a *AnchorAttributes) Matches(other Attributes) bool { + ot, ok := other.(*AnchorAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.HREF) > 0 && a.HREF != ot.HREF { + return false + } + + return true +} + +func (a *TextAttributes) Matches(other Attributes) bool { + ot, ok := other.(*TextAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + return true +} + +func (a *ScriptAttributes) Matches(other Attributes) bool { + ot, ok := other.(*ScriptAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if ot.Type != "" && a.Type != ot.Type { + return false + } + + if ot.Source != "" && a.Source != ot.Source { + return false + } + + return true +} + func (a *InputAttributes) SetValue(value interface{}) { a.FieldValue = value } diff --git a/ui/node/attributes_test.go b/ui/node/attributes_test.go index 218919e1145e..62c2316d3c9f 100644 --- a/ui/node/attributes_test.go +++ b/ui/node/attributes_test.go @@ -21,6 +21,70 @@ func TestIDs(t *testing.T) { assert.EqualValues(t, "foo", (&ScriptAttributes{Identifier: "foo"}).ID()) } +func TestMatchesAnchorAttributes(t *testing.T) { + assert.True(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{Identifier: "foo"})) + assert.True(t, (&AnchorAttributes{HREF: "bar"}).Matches(&AnchorAttributes{HREF: "bar"})) + assert.False(t, (&AnchorAttributes{HREF: "foo"}).Matches(&AnchorAttributes{HREF: "bar"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{HREF: "bar"})) + + assert.True(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "bar"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "baz"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "bar", HREF: "bar"})) + + assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) +} + +func TestMatchesImageAttributes(t *testing.T) { + assert.True(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"})) + assert.True(t, (&ImageAttributes{Source: "bar"}).Matches(&ImageAttributes{Source: "bar"})) + assert.False(t, (&ImageAttributes{Source: "foo"}).Matches(&ImageAttributes{Source: "bar"})) + assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Source: "bar"})) + + assert.True(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "bar"})) + assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "baz"})) + assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "bar", Source: "bar"})) + + assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) +} + +func TestMatchesInputAttributes(t *testing.T) { + // Test when other is not of type *InputAttributes + var attr Attributes = &ImageAttributes{} + inputAttr := &InputAttributes{Name: "foo"} + assert.False(t, inputAttr.Matches(attr)) + + // Test when ID is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText} + assert.False(t, inputAttr.Matches(attr)) + + // Test when Type is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeNumber} + assert.False(t, inputAttr.Matches(attr)) + + // Test when FieldValue is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "baz"} + assert.False(t, inputAttr.Matches(attr)) + + // Test when Name is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText} + assert.False(t, inputAttr.Matches(attr)) + + // Test when all fields are the same + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + assert.True(t, inputAttr.Matches(attr)) +} + +func TestMatchesTextAttributes(t *testing.T) { + assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) + assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) + assert.False(t, (&TextAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"})) +} + func TestNodeEncode(t *testing.T) { script := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ScriptAttributes{}}) assert.EqualValues(t, Script, gjson.Get(script, "attributes.node_type").String()) diff --git a/ui/node/node.go b/ui/node/node.go index e08295b827f4..cf0cedb79f94 100644 --- a/ui/node/node.go +++ b/ui/node/node.go @@ -49,6 +49,7 @@ const ( LookupGroup UiNodeGroup = "lookup_secret" WebAuthnGroup UiNodeGroup = "webauthn" PasskeyGroup UiNodeGroup = "passkey" + TwoStepGroup UiNodeGroup = "two_step" ) func (g UiNodeGroup) String() string { @@ -218,6 +219,7 @@ func SortUseOrder(keysInOrder []string) func(*sortOptions) { options.keysInOrder = keysInOrder } } + func SortUseOrderAppend(keysInOrder []string) func(*sortOptions) { return func(options *sortOptions) { options.keysInOrderAppend = keysInOrder @@ -353,6 +355,37 @@ func (n *Nodes) Append(node *Node) { *n = append(*n, node) } +func (n *Nodes) RemoveMatching(node *Node) { + if n == nil { + return + } + + var r Nodes + for k, v := range *n { + if !(*n)[k].Matches(node) { + r = append(r, v) + } + } + + *n = r +} + +func (n *Node) Matches(needle *Node) bool { + if len(needle.ID()) > 0 && n.ID() != needle.ID() { + return false + } + + if needle.Type != "" && n.Type != needle.Type { + return false + } + + if needle.Group != "" && n.Group != needle.Group { + return false + } + + return n.Attributes.Matches(needle.Attributes) +} + func (n *Node) UnmarshalJSON(data []byte) error { var attr Attributes switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) { diff --git a/ui/node/node_test.go b/ui/node/node_test.go index f8867b98c2a3..c9f1dca1838f 100644 --- a/ui/node/node_test.go +++ b/ui/node/node_test.go @@ -8,6 +8,7 @@ import ( "context" "embed" "encoding/json" + "github.com/ory/kratos/text" "path/filepath" "testing" @@ -193,3 +194,64 @@ func TestNodeJSON(t *testing.T) { require.EqualError(t, json.NewDecoder(bytes.NewReader(json.RawMessage(`{"type": "foo"}`))).Decode(&n), "unexpected node type: foo") }) } + +func TestMatchesNode(t *testing.T) { + // Test when ID is different + node1 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}} + assert.False(t, node1.Matches(node2)) + + // Test when Type is different + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Text, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.False(t, node1.Matches(node2)) + + // Test when Group is different + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Input, Group: node.OpenIDConnectGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.False(t, node1.Matches(node2)) + + // Test when all fields are the same + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.True(t, node1.Matches(node2)) +} + +func TestRemoveMatchingNodes(t *testing.T) { + nodes := node.Nodes{ + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}}, + } + + // Test when node to remove is present + nodeToRemove := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}} + nodes.RemoveMatching(nodeToRemove) + assert.Len(t, nodes, 2) + for _, n := range nodes { + assert.NotEqual(t, nodeToRemove.ID(), n.ID()) + } + + // Test when node to remove is not present + nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "qux"}} + nodes.RemoveMatching(nodeToRemove) + assert.Len(t, nodes, 2) // length should remain the same + + // Test when node to remove is present + nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}} + ui := &container.Container{ + Nodes: nodes, + } + + ui.GetNodes().RemoveMatching(nodeToRemove) + assert.Len(t, *ui.GetNodes(), 1) + for _, n := range *ui.GetNodes() { + assert.NotEqual(t, "bar", n.ID()) + assert.NotEqual(t, "baz", n.ID()) + } + + ui.Nodes.Append(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) + assert.NotNil(t, ui.Nodes.Find("method")) + ui.GetNodes().RemoveMatching(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit)) + assert.Nil(t, ui.Nodes.Find("method")) +} From 735fc5b2c5a99746d3012cc38ee2e1b7cc3a67f2 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:46:34 +0100 Subject: [PATCH 137/262] feat: add additional messages --- text/id.go | 1 + text/message_system.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/text/id.go b/text/id.go index edff417a2738..995d037e9606 100644 --- a/text/id.go +++ b/text/id.go @@ -201,4 +201,5 @@ const ( const ( ErrorSystem ID = 5000000 + iota ErrorSystemGeneric + ErrorSelfServiceNoMethodsAvailable ) diff --git a/text/message_system.go b/text/message_system.go index d94ce73f9872..b4b0f20659e7 100644 --- a/text/message_system.go +++ b/text/message_system.go @@ -13,3 +13,11 @@ func NewErrorSystemGeneric(reason string) *Message { }), } } + +func NewErrorSelfServiceNoMethodsAvailable() *Message { + return &Message{ + ID: ErrorSelfServiceNoMethodsAvailable, + Text: "No authentication methods are available for this request. Please contact the site or app owner.", + Type: Error, + } +} From 99c945c92d0c2745dc8df4402d755afd53e1b9aa Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:04:57 +0200 Subject: [PATCH 138/262] feat: add redirect to continue_with for SPA flows This patch adds the new `continue_with` action `redirect_browser_to`, which contains the redirect URL the app should redirect to. It is only supported for SPA (not server-side browser apps, not native apps) flows at this point in time. --- .schema/openapi/patches/schema.yaml | 2 + internal/client-go/.openapi-generator/FILES | 2 + internal/client-go/README.md | 1 + internal/client-go/model_continue_with.go | 40 +++++ ...model_continue_with_redirect_browser_to.go | 147 +++++++++++++++++ internal/httpclient/.openapi-generator/FILES | 2 + internal/httpclient/README.md | 1 + internal/httpclient/model_continue_with.go | 40 +++++ ...model_continue_with_redirect_browser_to.go | 147 +++++++++++++++++ selfservice/flow/continue_with.go | 28 ++++ selfservice/flow/login/hook.go | 4 + selfservice/flow/registration/hook.go | 7 +- selfservice/flow/settings/hook.go | 1 + .../strategy/code/strategy_login_test.go | 10 +- .../code/strategy_registration_test.go | 16 +- selfservice/strategy/lookup/login_test.go | 7 + selfservice/strategy/lookup/settings_test.go | 6 + .../strategy/passkey/passkey_login_test.go | 7 + .../passkey/passkey_registration_test.go | 9 ++ .../strategy/passkey/passkey_settings_test.go | 7 + selfservice/strategy/password/login_test.go | 26 +++ .../strategy/password/registration_test.go | 51 +++--- .../strategy/password/settings_test.go | 11 +- ...on=hydrate_the_proper_fields-type=spa.json | 153 ++++++++++++++++++ selfservice/strategy/profile/strategy_test.go | 9 +- selfservice/strategy/totp/login_test.go | 17 +- selfservice/strategy/totp/settings_test.go | 12 ++ selfservice/strategy/webauthn/login_test.go | 7 + .../strategy/webauthn/registration_test.go | 7 + .../strategy/webauthn/settings_test.go | 8 + spec/api.json | 20 +++ spec/swagger.json | 16 ++ 32 files changed, 790 insertions(+), 31 deletions(-) create mode 100644 internal/client-go/model_continue_with_redirect_browser_to.go create mode 100644 internal/httpclient/model_continue_with_redirect_browser_to.go create mode 100644 selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json diff --git a/.schema/openapi/patches/schema.yaml b/.schema/openapi/patches/schema.yaml index 206aceb2708e..ff661ce4079d 100644 --- a/.schema/openapi/patches/schema.yaml +++ b/.schema/openapi/patches/schema.yaml @@ -43,6 +43,7 @@ set_ory_session_token: "#/components/schemas/continueWithSetOrySessionToken" show_settings_ui: "#/components/schemas/continueWithSettingsUi" show_recovery_ui: "#/components/schemas/continueWithRecoveryUi" + redirect_browser_to: "#/components/schemas/continueWithRedirectBrowserTo" - op: add path: /components/schemas/continueWith/oneOf @@ -51,3 +52,4 @@ - "$ref": "#/components/schemas/continueWithSetOrySessionToken" - "$ref": "#/components/schemas/continueWithSettingsUi" - "$ref": "#/components/schemas/continueWithRecoveryUi" + - "$ref": "#/components/schemas/continueWithRedirectBrowserTo" diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index fdf34c5e1507..8f05b235508f 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md docs/ContinueWith.md docs/ContinueWithRecoveryUi.md docs/ContinueWithRecoveryUiFlow.md +docs/ContinueWithRedirectBrowserTo.md docs/ContinueWithSetOrySessionToken.md docs/ContinueWithSettingsUi.md docs/ContinueWithSettingsUiFlow.md @@ -139,6 +140,7 @@ model_consistency_request_parameters.go model_continue_with.go model_continue_with_recovery_ui.go model_continue_with_recovery_ui_flow.go +model_continue_with_redirect_browser_to.go model_continue_with_set_ory_session_token.go model_continue_with_settings_ui.go model_continue_with_settings_ui_flow.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index 04dd61ab7d1e..01f9831e7520 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -142,6 +142,7 @@ Class | Method | HTTP request | Description - [ContinueWith](docs/ContinueWith.md) - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) + - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) diff --git a/internal/client-go/model_continue_with.go b/internal/client-go/model_continue_with.go index 9e97dbf479e7..6fb1056836e6 100644 --- a/internal/client-go/model_continue_with.go +++ b/internal/client-go/model_continue_with.go @@ -19,6 +19,7 @@ import ( // ContinueWith - struct for ContinueWith type ContinueWith struct { ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken ContinueWithSettingsUi *ContinueWithSettingsUi ContinueWithVerificationUi *ContinueWithVerificationUi @@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit } } +// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith +func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith { + return ContinueWith{ + ContinueWithRedirectBrowserTo: v, + } +} + // ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith { return ContinueWith{ @@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") } + // check if the discriminator value is 'redirect_browser_to' + if jsonDict["action"] == "redirect_browser_to" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'set_ory_session_token' if jsonDict["action"] == "set_ory_session_token" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'continueWithRedirectBrowserTo' + if jsonDict["action"] == "continueWithRedirectBrowserTo" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'continueWithSetOrySessionToken' if jsonDict["action"] == "continueWithSetOrySessionToken" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) { return json.Marshal(&src.ContinueWithRecoveryUi) } + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + if src.ContinueWithSetOrySessionToken != nil { return json.Marshal(&src.ContinueWithSetOrySessionToken) } @@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} { return obj.ContinueWithRecoveryUi } + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + if obj.ContinueWithSetOrySessionToken != nil { return obj.ContinueWithSetOrySessionToken } diff --git a/internal/client-go/model_continue_with_redirect_browser_to.go b/internal/client-go/model_continue_with_redirect_browser_to.go new file mode 100644 index 000000000000..46344016b779 --- /dev/null +++ b/internal/client-go/model_continue_with_redirect_browser_to.go @@ -0,0 +1,147 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` + Action interface{} `json:"action"` + // The URL to redirect the browser to + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` +} + +// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + this.Action = action + return &this +} + +// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + return &this +} + +// GetAction returns the Action field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { + if o == nil || o.Action == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { + o.Action = v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { + if o == nil || o.RedirectBrowserTo == nil { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || o.RedirectBrowserTo == nil { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { + if o != nil && o.RedirectBrowserTo != nil { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Action != nil { + toSerialize["action"] = o.Action + } + if o.RedirectBrowserTo != nil { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + return json.Marshal(toSerialize) +} + +type NullableContinueWithRedirectBrowserTo struct { + value *ContinueWithRedirectBrowserTo + isSet bool +} + +func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo { + return v.value +} + +func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRedirectBrowserTo) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRedirectBrowserTo) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo { + return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true} +} + +func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index fdf34c5e1507..8f05b235508f 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md docs/ContinueWith.md docs/ContinueWithRecoveryUi.md docs/ContinueWithRecoveryUiFlow.md +docs/ContinueWithRedirectBrowserTo.md docs/ContinueWithSetOrySessionToken.md docs/ContinueWithSettingsUi.md docs/ContinueWithSettingsUiFlow.md @@ -139,6 +140,7 @@ model_consistency_request_parameters.go model_continue_with.go model_continue_with_recovery_ui.go model_continue_with_recovery_ui_flow.go +model_continue_with_redirect_browser_to.go model_continue_with_set_ory_session_token.go model_continue_with_settings_ui.go model_continue_with_settings_ui_flow.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 04dd61ab7d1e..01f9831e7520 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -142,6 +142,7 @@ Class | Method | HTTP request | Description - [ContinueWith](docs/ContinueWith.md) - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) + - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) diff --git a/internal/httpclient/model_continue_with.go b/internal/httpclient/model_continue_with.go index 9e97dbf479e7..6fb1056836e6 100644 --- a/internal/httpclient/model_continue_with.go +++ b/internal/httpclient/model_continue_with.go @@ -19,6 +19,7 @@ import ( // ContinueWith - struct for ContinueWith type ContinueWith struct { ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken ContinueWithSettingsUi *ContinueWithSettingsUi ContinueWithVerificationUi *ContinueWithVerificationUi @@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit } } +// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith +func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith { + return ContinueWith{ + ContinueWithRedirectBrowserTo: v, + } +} + // ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith { return ContinueWith{ @@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") } + // check if the discriminator value is 'redirect_browser_to' + if jsonDict["action"] == "redirect_browser_to" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'set_ory_session_token' if jsonDict["action"] == "set_ory_session_token" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'continueWithRedirectBrowserTo' + if jsonDict["action"] == "continueWithRedirectBrowserTo" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'continueWithSetOrySessionToken' if jsonDict["action"] == "continueWithSetOrySessionToken" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) { return json.Marshal(&src.ContinueWithRecoveryUi) } + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + if src.ContinueWithSetOrySessionToken != nil { return json.Marshal(&src.ContinueWithSetOrySessionToken) } @@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} { return obj.ContinueWithRecoveryUi } + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + if obj.ContinueWithSetOrySessionToken != nil { return obj.ContinueWithSetOrySessionToken } diff --git a/internal/httpclient/model_continue_with_redirect_browser_to.go b/internal/httpclient/model_continue_with_redirect_browser_to.go new file mode 100644 index 000000000000..46344016b779 --- /dev/null +++ b/internal/httpclient/model_continue_with_redirect_browser_to.go @@ -0,0 +1,147 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` + Action interface{} `json:"action"` + // The URL to redirect the browser to + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` +} + +// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + this.Action = action + return &this +} + +// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + return &this +} + +// GetAction returns the Action field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { + if o == nil || o.Action == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { + o.Action = v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { + if o == nil || o.RedirectBrowserTo == nil { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || o.RedirectBrowserTo == nil { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { + if o != nil && o.RedirectBrowserTo != nil { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Action != nil { + toSerialize["action"] = o.Action + } + if o.RedirectBrowserTo != nil { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + return json.Marshal(toSerialize) +} + +type NullableContinueWithRedirectBrowserTo struct { + value *ContinueWithRedirectBrowserTo + isSet bool +} + +func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo { + return v.value +} + +func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRedirectBrowserTo) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRedirectBrowserTo) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo { + return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true} +} + +func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go index 7a5f9ce22410..5b56bbf9aab6 100644 --- a/selfservice/flow/continue_with.go +++ b/selfservice/flow/continue_with.go @@ -201,6 +201,34 @@ func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI { } } +// swagger:enum ContinueWithActionRedirectTo +type ContinueWithActionRedirectBrowserTo string + +// #nosec G101 -- only a key constant +const ( + ContinueWithActionRedirectBrowserToString ContinueWithActionRedirectBrowserTo = "redirect_browser_to" +) + +// Indicates, that the UI flow could be continued by showing a recovery ui +// +// swagger:model continueWithRedirectBrowserTo +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` + // + // required: true + Action ContinueWithActionRedirectBrowserTo `json:"action"` + + // The URL to redirect the browser to + RedirectTo string `json:"redirect_browser_to"` +} + +func NewContinueWithRedirectBrowserTo(redirectTo string) *ContinueWithRedirectBrowserTo { + return &ContinueWithRedirectBrowserTo{ + Action: ContinueWithActionRedirectBrowserToString, + RedirectTo: redirectTo, + } +} + func ErrorWithContinueWith(err *herodot.DefaultError, continueWith ...ContinueWith) *herodot.DefaultError { if err.DetailsField == nil { err.DetailsField = map[string]interface{}{} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index f0e06ccfc934..4d3deddf2a0a 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -159,6 +159,10 @@ func (e *HookExecutor) PostLoginHook( "redirect_reason": "login successful", })...) + if f.Type != flow.TypeAPI { + f.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) + } + classified := s s = s.Declassified() diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index 6a997009c1c5..e44be2487bbb 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -192,12 +192,17 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque if err != nil { return err } + span.SetAttributes(otelx.StringAttrs(map[string]string{ "return_to": returnTo.String(), - "flow_type": string(flow.TypeBrowser), + "flow_type": string(registrationFlow.Type), "redirect_reason": "registration successful", })...) + if registrationFlow.Type == flow.TypeBrowser && x.IsJSONRequest(r) { + registrationFlow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) + } + e.d.Audit(). WithRequest(r). WithField("identity_id", i.ID). diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go index b688fd0fc431..88741e766736 100644 --- a/selfservice/flow/settings/hook.go +++ b/selfservice/flow/settings/hook.go @@ -308,6 +308,7 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, } // ContinueWith items are transient items, not stored in the database, and need to be carried over here, so // they can be returned to the client. + ctxUpdate.Flow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems e.d.Writer().Write(w, r, updatedFlow) diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index 19cac6d38375..55f090c19dd5 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -12,6 +12,8 @@ import ( "net/url" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/x/ioutilx" "github.com/ory/x/snapshotx" "github.com/ory/x/sqlcon" @@ -247,9 +249,15 @@ func TestLoginCodeStrategy(t *testing.T) { assert.NotEmpty(t, loginCode) // 3. Submit OTP - submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + state := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { v.Set("code", loginCode) }, true, nil) + if tc.apiType == ApiTypeSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body) + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } }) t.Run("case=new identities automatically have login with code", func(t *testing.T) { diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index 27c645a94190..0b6caaa15da1 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -15,6 +15,8 @@ import ( "strings" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" @@ -37,6 +39,7 @@ type state struct { email string testServer *httptest.Server resultIdentity *identity.Identity + body string } func TestRegistrationCodeStrategyDisabled(t *testing.T) { @@ -172,6 +175,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { values.Set("method", "code") body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values)) + s.body = body if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) @@ -213,6 +217,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { vals(&values) body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values)) + s.body = body if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) @@ -240,7 +245,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { t.Parallel() ctx := context.Background() - _, reg, public := setup(ctx, t) + conf, reg, public := setup(ctx, t) for _, tc := range []struct { d string @@ -279,6 +284,15 @@ func TestRegistrationCodeStrategy(t *testing.T) { state = submitOTP(ctx, t, reg, state, func(v *url.Values) { v.Set("code", registrationCode) }, tc.apiType, nil) + + if tc.apiType == ApiTypeSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body) + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else if tc.apiType == ApiTypeSPA { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } else if tc.apiType == ApiTypeNative { + assert.NotContains(t, gjson.Get(state.body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", state.body) + } }) t.Run("case=should normalize email address on sign up", func(t *testing.T) { diff --git a/selfservice/strategy/lookup/login_test.go b/selfservice/strategy/lookup/login_test.go index c4896962c660..b3bc454dac70 100644 --- a/selfservice/strategy/lookup/login_test.go +++ b/selfservice/strategy/lookup/login_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -241,6 +243,7 @@ func TestCompleteLogin(t *testing.T) { // We can still use another key body, res = doAPIFlowWithClient(t, payload("key-2"), id, apiClient, true) check(t, false, body, res, "key-2", 3) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { @@ -250,6 +253,7 @@ func TestCompleteLogin(t *testing.T) { // We can still use another key body, res = doBrowserFlowWithClient(t, false, payload("key-5"), id, browserClient, true) check(t, true, body, res, "key-5", 3) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { @@ -259,6 +263,9 @@ func TestCompleteLogin(t *testing.T) { // We can still use another key body, res = doBrowserFlowWithClient(t, true, payload("key-8"), id, browserClient, true) check(t, false, body, res, "key-8", 3) + + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) }) }) diff --git a/selfservice/strategy/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go index fce2be4c0974..48a7faf19705 100644 --- a/selfservice/strategy/lookup/settings_test.go +++ b/selfservice/strategy/lookup/settings_test.go @@ -423,8 +423,11 @@ func TestCompleteSettings(t *testing.T) { if spa { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) } assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) @@ -508,8 +511,11 @@ func TestCompleteSettings(t *testing.T) { if spa { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) } assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go index 2a6c2075557e..67ec6737f3ba 100644 --- a/selfservice/strategy/passkey/passkey_login_test.go +++ b/selfservice/strategy/passkey/passkey_login_test.go @@ -209,7 +209,14 @@ func TestCompleteLogin(t *testing.T) { actualFlow, err := fix.reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) require.NoError(t, err) + assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } // We test here that login works even if the identity schema contains diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go index d495e8c4dfe4..d7191207cedb 100644 --- a/selfservice/strategy/passkey/passkey_registration_test.go +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -8,6 +8,8 @@ import ( "net/url" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -327,6 +329,13 @@ func TestRegistration(t *testing.T) { i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) require.NoError(t, err) assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + + if f == "spa" { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), fix.redirNoSessionTS.URL+"/registration-return-ts", "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } }) } }) diff --git a/selfservice/strategy/passkey/passkey_settings_test.go b/selfservice/strategy/passkey/passkey_settings_test.go index ced111071711..842f4d22c10e 100644 --- a/selfservice/strategy/passkey/passkey_settings_test.go +++ b/selfservice/strategy/passkey/passkey_settings_test.go @@ -271,6 +271,13 @@ func TestCompleteSettings(t *testing.T) { flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) testhelpers.EnsureAAL(t, browserClient, fix.publicTS, "aal1", string(identity.CredentialsTypePasskey)) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.uiTS.URL, "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } t.Run("type=browser", func(t *testing.T) { diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index 8c2f2cb73245..df2fe5e29cae 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -737,6 +737,32 @@ func TestCompleteLogin(t *testing.T) { assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) }) + t.Run("should succeed and include redirect continue_with in SPA flow", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false) + values := url.Values{"method": {"password"}, "identifier": {strings.ToUpper(identifier)}, "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode() + body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) + }) + + t.Run("should succeed and not have redirect continue_with in api flow", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false) + + body, res := testhelpers.LoginMakeRequest(t, true, true, f, browserClient, fmt.Sprintf(`{"method":"password","identifier":"%s","password":"%s"}`, strings.ToUpper(identifier), pwd)) + + assert.EqualValues(t, http.StatusOK, res.StatusCode, body) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + }) + t.Run("should login even if old form field name is used", func(t *testing.T) { identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index 14bf2382b212..d52ca2d77707 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/driver" "github.com/ory/kratos/internal/registrationhelpers" @@ -106,7 +108,7 @@ func TestRegistration(t *testing.T) { }) }) - var expectLoginBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectRegistrationBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { if isAPI { return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values, isSPA, http.StatusOK, @@ -126,17 +128,17 @@ func TestRegistration(t *testing.T) { isSPA, http.StatusOK, expectReturnTo) } - var expectSuccessfulLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectSuccessfulRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirTS) - return expectLoginBody(t, redirTS, isAPI, isSPA, hc, values) + return expectRegistrationBody(t, redirTS, isAPI, isSPA, hc, values) } - var expectNoLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectNoRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirNoSessionTS) t.Cleanup(func() { useReturnToFromTS(redirTS) }) - return expectLoginBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) + return expectRegistrationBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) } t.Run("case=should reject invalid transient payload", func(t *testing.T) { @@ -178,7 +180,7 @@ func TestRegistration(t *testing.T) { t.Run("type=api", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -188,7 +190,7 @@ func TestRegistration(t *testing.T) { t.Run("type=spa", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -198,7 +200,7 @@ func TestRegistration(t *testing.T) { t.Run("type=browser", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -213,7 +215,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=api", func(t *testing.T) { - body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -221,10 +223,11 @@ func TestRegistration(t *testing.T) { assert.Equal(t, `registration-identifier-8-api`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + assert.NotContains(t, gjson.Get(body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", body) }) t.Run("type=spa", func(t *testing.T) { - body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -232,15 +235,17 @@ func TestRegistration(t *testing.T) { assert.Equal(t, `registration-identifier-8-spa`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { - body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-8-browser`, gjson.Get(body, "identity.traits.username").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) }) @@ -249,7 +254,7 @@ func TestRegistration(t *testing.T) { conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) t.Run("type=api", func(t *testing.T) { - body := expectNoLogin(t, true, false, nil, func(v url.Values) { + body := expectNoRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -260,7 +265,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - expectNoLogin(t, false, true, nil, func(v url.Values) { + expectNoRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -268,7 +273,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - expectNoLogin(t, false, false, nil, func(v url.Values) { + expectNoRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -300,7 +305,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, true, false, apiClient, values) + _ = expectSuccessfulRegistration(t, true, false, apiClient, values) body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS, applyTransform(values, transform), false, http.StatusBadRequest, publicTS.URL+registration.RouteSubmitFlow) @@ -314,7 +319,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, false, true, nil, values) + _ = expectSuccessfulRegistration(t, false, true, nil, values) body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "spa", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-spa-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) @@ -326,7 +331,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, false, false, nil, values) + _ = expectSuccessfulRegistration(t, false, false, nil, values) body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "browser", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-browser-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) @@ -541,7 +546,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=api", func(t *testing.T) { - actual := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -550,7 +555,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -559,7 +564,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -620,7 +625,7 @@ func TestRegistration(t *testing.T) { username := "registration-custom-schema" t.Run("type=api", func(t *testing.T) { - body := expectNoLogin(t, true, false, nil, func(v url.Values) { + body := expectNoRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", username+"-api") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") @@ -631,7 +636,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - expectNoLogin(t, false, true, nil, func(v url.Values) { + expectNoRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", username+"-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") @@ -639,7 +644,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - expectNoLogin(t, false, false, nil, func(v url.Values) { + expectNoRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", username+"-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go index a4ee7e6c7fa0..49912ee95418 100644 --- a/selfservice/strategy/password/settings_test.go +++ b/selfservice/strategy/password/settings_test.go @@ -13,6 +13,8 @@ import ( "strings" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/internal/settingshelpers" "github.com/ory/kratos/text" @@ -82,7 +84,7 @@ func TestSettings(t *testing.T) { testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true) - _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + settingsUI := testhelpers.NewSettingsUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) _ = testhelpers.NewLoginUIWith401Response(t, conf) conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m") @@ -242,15 +244,20 @@ func TestSettings(t *testing.T) { t.Run("type=api", func(t *testing.T) { actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow) check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=spa", func(t *testing.T) { actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow) check(t, actual) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), settingsUI.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { - check(t, testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String())) + actual := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String()) + check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json new file mode 100644 index 000000000000..d6665e756663 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json @@ -0,0 +1,153 @@ +{ + "method": "POST", + "nodes": [ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.email", + "node_type": "input", + "type": "text" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.stringy", + "node_type": "input", + "type": "text", + "value": "foobar" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.numby", + "node_type": "input", + "type": "number", + "value": 2.5 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.booly", + "node_type": "input", + "type": "checkbox", + "value": false + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_big_number", + "node_type": "input", + "type": "number", + "value": 2048 + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.should_long_string", + "node_type": "input", + "type": "text", + "value": "asdfasdfasdfasdfasfdasdfasdfasdf" + }, + "group": "profile", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "profile" + }, + "group": "profile", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070003, + "text": "Save", + "type": "info" + } + }, + "type": "input" + } + ] +} diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go index 7d0c831711c3..92351d5b8d72 100644 --- a/selfservice/strategy/profile/strategy_test.go +++ b/selfservice/strategy/profile/strategy_test.go @@ -210,7 +210,7 @@ func TestStrategyTraits(t *testing.T) { run(t, apiIdentity1, pr, settings.RouteInitAPIFlow) }) - t.Run("type=api", func(t *testing.T) { + t.Run("type=spa", func(t *testing.T) { pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendApi.CreateBrowserSettingsFlow(context.Background()).Execute() require.NoError(t, err) run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow) @@ -449,15 +449,20 @@ func TestStrategyTraits(t *testing.T) { t.Run("type=api", func(t *testing.T) { actual := expectSuccess(t, true, false, apiUser1, payload("not-john-doe-api@mail.com")) check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=sqa", func(t *testing.T) { actual := expectSuccess(t, false, true, browserUser1, payload("not-john-doe-browser@mail.com")) check(t, actual) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), ui.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { - check(t, expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com"))) + actual := expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com")) + check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) diff --git a/selfservice/strategy/totp/login_test.go b/selfservice/strategy/totp/login_test.go index 6456ea7cc599..2eb434256ed8 100644 --- a/selfservice/strategy/totp/login_test.go +++ b/selfservice/strategy/totp/login_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/x/assertx" "github.com/gofrs/uuid" @@ -333,23 +335,36 @@ func TestCompleteLogin(t *testing.T) { t.Run("type=api", func(t *testing.T) { body, res := doAPIFlow(t, payload, id) check(t, false, body, res) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { body, res := doBrowserFlow(t, false, payload, id, "") check(t, true, body, res) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser set return_to", func(t *testing.T) { returnTo := "https://www.ory.sh" - _, res := doBrowserFlow(t, false, payload, id, returnTo) + body, res := doBrowserFlow(t, false, payload, id, returnTo) t.Log(res.Request.URL.String()) assert.Contains(t, res.Request.URL.String(), returnTo) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { body, res := doBrowserFlow(t, true, payload, id, "") check(t, false, body, res) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) + }) + + t.Run("type=spa set return_to", func(t *testing.T) { + returnTo := "https://www.ory.sh" + body, res := doBrowserFlow(t, true, payload, id, returnTo) + check(t, false, body, res) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, returnTo, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) }) }) diff --git a/selfservice/strategy/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go index 0fd479f1b220..43d41b2f66aa 100644 --- a/selfservice/strategy/totp/settings_test.go +++ b/selfservice/strategy/totp/settings_test.go @@ -241,6 +241,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=spa", func(t *testing.T) { @@ -250,6 +251,9 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { @@ -259,6 +263,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) @@ -344,6 +349,13 @@ func TestCompleteSettings(t *testing.T) { checkIdentity(t, id, key) testhelpers.EnsureAAL(t, hc, publicTS, "aal2", string(identity.CredentialsTypeTOTP)) + + if isSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } } t.Run("type=api", func(t *testing.T) { diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go index f5d332182163..46db972c4cc7 100644 --- a/selfservice/strategy/webauthn/login_test.go +++ b/selfservice/strategy/webauthn/login_test.go @@ -446,6 +446,13 @@ func TestCompleteLogin(t *testing.T) { actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) require.NoError(t, err) assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData))) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } t.Run("type=browser", func(t *testing.T) { diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index c0503b151ed8..973e1ae0ec81 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -367,6 +367,13 @@ func TestRegistration(t *testing.T) { i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email) require.NoError(t, err) assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + + if f == "spa" { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), redirNoSessionTS.URL+"/registration-return-ts", "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } }) } }) diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index acf4fd357b1d..3b46cc8de752 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -465,6 +465,13 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) @@ -474,6 +481,7 @@ func TestCompleteSettings(t *testing.T) { // Check not to remove other credentials with webauthn _, ok = actual.GetCredentials(identity.CredentialsTypePassword) assert.True(t, ok) + } t.Run("type=browser", func(t *testing.T) { diff --git a/spec/api.json b/spec/api.json index 1bb1345dc25c..3552ad49eb12 100644 --- a/spec/api.json +++ b/spec/api.json @@ -465,6 +465,7 @@ "continueWith": { "discriminator": { "mapping": { + "redirect_browser_to": "#/components/schemas/continueWithRedirectBrowserTo", "set_ory_session_token": "#/components/schemas/continueWithSetOrySessionToken", "show_recovery_ui": "#/components/schemas/continueWithRecoveryUi", "show_settings_ui": "#/components/schemas/continueWithSettingsUi", @@ -484,6 +485,9 @@ }, { "$ref": "#/components/schemas/continueWithRecoveryUi" + }, + { + "$ref": "#/components/schemas/continueWithRedirectBrowserTo" } ] }, @@ -525,6 +529,22 @@ ], "type": "object" }, + "continueWithRedirectBrowserTo": { + "description": "Indicates, that the UI flow could be continued by showing a recovery ui", + "properties": { + "action": { + "description": "Action will always be `redirect_browser_to`" + }, + "redirect_browser_to": { + "description": "The URL to redirect the browser to", + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, "continueWithSetOrySessionToken": { "description": "Indicates that a session was issued, and the application should use this token for authenticated requests", "properties": { diff --git a/spec/swagger.json b/spec/swagger.json index e790c71fecb4..50cb2858b4a1 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3659,6 +3659,22 @@ } } }, + "continueWithRedirectBrowserTo": { + "description": "Indicates, that the UI flow could be continued by showing a recovery ui", + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "description": "Action will always be `redirect_browser_to`" + }, + "redirect_browser_to": { + "description": "The URL to redirect the browser to", + "type": "string" + } + } + }, "continueWithSetOrySessionToken": { "description": "Indicates that a session was issued, and the application should use this token for authenticated requests", "type": "object", From 7b636d860c6917cb1133d6d1d7401808adb890c7 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:00:49 +0200 Subject: [PATCH 139/262] feat: add browser return_to continue_with action --- .../model_continue_with_recovery_ui_flow.go | 2 +- ...model_continue_with_redirect_browser_to.go | 51 ++++++++----------- .../model_continue_with_settings_ui_flow.go | 37 ++++++++++++++ ...odel_continue_with_verification_ui_flow.go | 2 +- .../model_continue_with_recovery_ui_flow.go | 2 +- ...model_continue_with_redirect_browser_to.go | 51 ++++++++----------- .../model_continue_with_settings_ui_flow.go | 37 ++++++++++++++ ...odel_continue_with_verification_ui_flow.go | 2 +- selfservice/flow/continue_with.go | 23 +++++++-- .../strategy/code/strategy_recovery.go | 5 +- spec/api.json | 18 +++++-- spec/swagger.json | 18 +++++-- 12 files changed, 171 insertions(+), 77 deletions(-) diff --git a/internal/client-go/model_continue_with_recovery_ui_flow.go b/internal/client-go/model_continue_with_recovery_ui_flow.go index 3fde7e717ef2..251725a73c3b 100644 --- a/internal/client-go/model_continue_with_recovery_ui_flow.go +++ b/internal/client-go/model_continue_with_recovery_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithRecoveryUiFlow struct { // The ID of the recovery flow Id string `json:"id"` - // The URL of the recovery flow + // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` } diff --git a/internal/client-go/model_continue_with_redirect_browser_to.go b/internal/client-go/model_continue_with_redirect_browser_to.go index 46344016b779..20c3e4f3c562 100644 --- a/internal/client-go/model_continue_with_redirect_browser_to.go +++ b/internal/client-go/model_continue_with_redirect_browser_to.go @@ -17,19 +17,20 @@ import ( // ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui type ContinueWithRedirectBrowserTo struct { - // Action will always be `redirect_browser_to` - Action interface{} `json:"action"` + // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString + Action string `json:"action"` // The URL to redirect the browser to - RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + RedirectBrowserTo string `json:"redirect_browser_to"` } // NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { +func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo { this := ContinueWithRedirectBrowserTo{} this.Action = action + this.RedirectBrowserTo = redirectBrowserTo return &this } @@ -42,10 +43,9 @@ func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowser } // GetAction returns the Action field value -// If the value is explicit nil, the zero value for interface{} will be returned -func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { +func (o *ContinueWithRedirectBrowserTo) GetAction() string { if o == nil { - var ret interface{} + var ret string return ret } @@ -54,57 +54,48 @@ func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { // GetActionOk returns a tuple with the Action field value // and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { - if o == nil || o.Action == nil { +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) { + if o == nil { return nil, false } return &o.Action, true } // SetAction sets field value -func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { +func (o *ContinueWithRedirectBrowserTo) SetAction(v string) { o.Action = v } -// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +// GetRedirectBrowserTo returns the RedirectBrowserTo field value func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { var ret string return ret } - return *o.RedirectBrowserTo + + return o.RedirectBrowserTo } -// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value // and a boolean to check if the value has been set. func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { return nil, false } - return o.RedirectBrowserTo, true -} - -// HasRedirectBrowserTo returns a boolean if a field has been set. -func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { - if o != nil && o.RedirectBrowserTo != nil { - return true - } - - return false + return &o.RedirectBrowserTo, true } -// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +// SetRedirectBrowserTo sets field value func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { - o.RedirectBrowserTo = &v + o.RedirectBrowserTo = v } func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.Action != nil { + if true { toSerialize["action"] = o.Action } - if o.RedirectBrowserTo != nil { + if true { toSerialize["redirect_browser_to"] = o.RedirectBrowserTo } return json.Marshal(toSerialize) diff --git a/internal/client-go/model_continue_with_settings_ui_flow.go b/internal/client-go/model_continue_with_settings_ui_flow.go index 4ccaf74ef1b8..d6e9b9441f99 100644 --- a/internal/client-go/model_continue_with_settings_ui_flow.go +++ b/internal/client-go/model_continue_with_settings_ui_flow.go @@ -19,6 +19,8 @@ import ( type ContinueWithSettingsUiFlow struct { // The ID of the settings flow Id string `json:"id"` + // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + Url *string `json:"url,omitempty"` } // NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object @@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) { o.Id = v } +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithSettingsUiFlow) GetUrl() string { + if o == nil || o.Url == nil { + var ret string + return ret + } + return *o.Url +} + +// GetUrlOk returns a tuple with the Url field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) { + if o == nil || o.Url == nil { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithSettingsUiFlow) HasUrl() bool { + if o != nil && o.Url != nil { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithSettingsUiFlow) SetUrl(v string) { + o.Url = &v +} + func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if true { toSerialize["id"] = o.Id } + if o.Url != nil { + toSerialize["url"] = o.Url + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_continue_with_verification_ui_flow.go b/internal/client-go/model_continue_with_verification_ui_flow.go index 8fdd4609cf93..3c73a0761339 100644 --- a/internal/client-go/model_continue_with_verification_ui_flow.go +++ b/internal/client-go/model_continue_with_verification_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithVerificationUiFlow struct { // The ID of the verification flow Id string `json:"id"` - // The URL of the verification flow + // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` // The address that should be verified in this flow VerifiableAddress string `json:"verifiable_address"` diff --git a/internal/httpclient/model_continue_with_recovery_ui_flow.go b/internal/httpclient/model_continue_with_recovery_ui_flow.go index 3fde7e717ef2..251725a73c3b 100644 --- a/internal/httpclient/model_continue_with_recovery_ui_flow.go +++ b/internal/httpclient/model_continue_with_recovery_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithRecoveryUiFlow struct { // The ID of the recovery flow Id string `json:"id"` - // The URL of the recovery flow + // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` } diff --git a/internal/httpclient/model_continue_with_redirect_browser_to.go b/internal/httpclient/model_continue_with_redirect_browser_to.go index 46344016b779..20c3e4f3c562 100644 --- a/internal/httpclient/model_continue_with_redirect_browser_to.go +++ b/internal/httpclient/model_continue_with_redirect_browser_to.go @@ -17,19 +17,20 @@ import ( // ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui type ContinueWithRedirectBrowserTo struct { - // Action will always be `redirect_browser_to` - Action interface{} `json:"action"` + // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString + Action string `json:"action"` // The URL to redirect the browser to - RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + RedirectBrowserTo string `json:"redirect_browser_to"` } // NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { +func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo { this := ContinueWithRedirectBrowserTo{} this.Action = action + this.RedirectBrowserTo = redirectBrowserTo return &this } @@ -42,10 +43,9 @@ func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowser } // GetAction returns the Action field value -// If the value is explicit nil, the zero value for interface{} will be returned -func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { +func (o *ContinueWithRedirectBrowserTo) GetAction() string { if o == nil { - var ret interface{} + var ret string return ret } @@ -54,57 +54,48 @@ func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { // GetActionOk returns a tuple with the Action field value // and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { - if o == nil || o.Action == nil { +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) { + if o == nil { return nil, false } return &o.Action, true } // SetAction sets field value -func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { +func (o *ContinueWithRedirectBrowserTo) SetAction(v string) { o.Action = v } -// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +// GetRedirectBrowserTo returns the RedirectBrowserTo field value func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { var ret string return ret } - return *o.RedirectBrowserTo + + return o.RedirectBrowserTo } -// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value // and a boolean to check if the value has been set. func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { return nil, false } - return o.RedirectBrowserTo, true -} - -// HasRedirectBrowserTo returns a boolean if a field has been set. -func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { - if o != nil && o.RedirectBrowserTo != nil { - return true - } - - return false + return &o.RedirectBrowserTo, true } -// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +// SetRedirectBrowserTo sets field value func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { - o.RedirectBrowserTo = &v + o.RedirectBrowserTo = v } func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.Action != nil { + if true { toSerialize["action"] = o.Action } - if o.RedirectBrowserTo != nil { + if true { toSerialize["redirect_browser_to"] = o.RedirectBrowserTo } return json.Marshal(toSerialize) diff --git a/internal/httpclient/model_continue_with_settings_ui_flow.go b/internal/httpclient/model_continue_with_settings_ui_flow.go index 4ccaf74ef1b8..d6e9b9441f99 100644 --- a/internal/httpclient/model_continue_with_settings_ui_flow.go +++ b/internal/httpclient/model_continue_with_settings_ui_flow.go @@ -19,6 +19,8 @@ import ( type ContinueWithSettingsUiFlow struct { // The ID of the settings flow Id string `json:"id"` + // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + Url *string `json:"url,omitempty"` } // NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object @@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) { o.Id = v } +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithSettingsUiFlow) GetUrl() string { + if o == nil || o.Url == nil { + var ret string + return ret + } + return *o.Url +} + +// GetUrlOk returns a tuple with the Url field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) { + if o == nil || o.Url == nil { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithSettingsUiFlow) HasUrl() bool { + if o != nil && o.Url != nil { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithSettingsUiFlow) SetUrl(v string) { + o.Url = &v +} + func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if true { toSerialize["id"] = o.Id } + if o.Url != nil { + toSerialize["url"] = o.Url + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_continue_with_verification_ui_flow.go b/internal/httpclient/model_continue_with_verification_ui_flow.go index 8fdd4609cf93..3c73a0761339 100644 --- a/internal/httpclient/model_continue_with_verification_ui_flow.go +++ b/internal/httpclient/model_continue_with_verification_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithVerificationUiFlow struct { // The ID of the verification flow Id string `json:"id"` - // The URL of the verification flow + // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` // The address that should be verified in this flow VerifiableAddress string `json:"verifiable_address"` diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go index 5b56bbf9aab6..9bc9e6152d78 100644 --- a/selfservice/flow/continue_with.go +++ b/selfservice/flow/continue_with.go @@ -89,6 +89,8 @@ type ContinueWithVerificationUIFlow struct { // The URL of the verification flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: false URL string `json:"url,omitempty"` } @@ -134,8 +136,11 @@ type ContinueWithSettingsUI struct { // // required: true Action ContinueWithActionShowSettingsUI `json:"action"` + // Flow contains the ID of the verification flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: true Flow ContinueWithSettingsUIFlow `json:"flow"` } @@ -146,13 +151,21 @@ type ContinueWithSettingsUIFlow struct { // // required: true ID uuid.UUID `json:"id"` + + // The URL of the settings flow + // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // + // required: false + URL string `json:"url,omitempty"` } -func NewContinueWithSettingsUI(f Flow) *ContinueWithSettingsUI { +func NewContinueWithSettingsUI(f Flow, redirectTo string) *ContinueWithSettingsUI { return &ContinueWithSettingsUI{ Action: ContinueWithActionShowSettingsUIString, Flow: ContinueWithSettingsUIFlow{ - ID: f.GetID(), + ID: f.GetID(), + URL: redirectTo, }, } } @@ -188,6 +201,8 @@ type ContinueWithRecoveryUIFlow struct { // The URL of the recovery flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: false URL string `json:"url,omitempty"` } @@ -201,7 +216,7 @@ func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI { } } -// swagger:enum ContinueWithActionRedirectTo +// swagger:enum ContinueWithActionRedirectBrowserTo type ContinueWithActionRedirectBrowserTo string // #nosec G101 -- only a key constant @@ -219,6 +234,8 @@ type ContinueWithRedirectBrowserTo struct { Action ContinueWithActionRedirectBrowserTo `json:"action"` // The URL to redirect the browser to + // + // required: true RedirectTo string `json:"redirect_browser_to"` } diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 758e81d04fd9..9376a8e1ef4d 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -235,12 +235,13 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, } if s.deps.Config().UseContinueWithTransitions(ctx) { + redirectTo := sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String() switch { case f.Type.IsAPI(), x.IsJSONRequest(r): - f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf)) + f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf, redirectTo)) s.deps.Writer().Write(w, r, f) default: - http.Redirect(w, r, sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String(), http.StatusSeeOther) + http.Redirect(w, r, redirectTo, http.StatusSeeOther) } } else { if x.IsJSONRequest(r) { diff --git a/spec/api.json b/spec/api.json index 3552ad49eb12..a78fe5793d00 100644 --- a/spec/api.json +++ b/spec/api.json @@ -520,7 +520,7 @@ "type": "string" }, "url": { - "description": "The URL of the recovery flow", + "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" } }, @@ -533,7 +533,12 @@ "description": "Indicates, that the UI flow could be continued by showing a recovery ui", "properties": { "action": { - "description": "Action will always be `redirect_browser_to`" + "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString", + "enum": [ + "redirect_browser_to" + ], + "type": "string", + "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString" }, "redirect_browser_to": { "description": "The URL to redirect the browser to", @@ -541,7 +546,8 @@ } }, "required": [ - "action" + "action", + "redirect_browser_to" ], "type": "object" }, @@ -594,6 +600,10 @@ "description": "The ID of the settings flow", "format": "uuid", "type": "string" + }, + "url": { + "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", + "type": "string" } }, "required": [ @@ -630,7 +640,7 @@ "type": "string" }, "url": { - "description": "The URL of the verification flow", + "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" }, "verifiable_address": { diff --git a/spec/swagger.json b/spec/swagger.json index 50cb2858b4a1..fe16afa03c25 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3654,7 +3654,7 @@ "format": "uuid" }, "url": { - "description": "The URL of the recovery flow", + "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" } } @@ -3663,11 +3663,17 @@ "description": "Indicates, that the UI flow could be continued by showing a recovery ui", "type": "object", "required": [ - "action" + "action", + "redirect_browser_to" ], "properties": { "action": { - "description": "Action will always be `redirect_browser_to`" + "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString", + "type": "string", + "enum": [ + "redirect_browser_to" + ], + "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString" }, "redirect_browser_to": { "description": "The URL to redirect the browser to", @@ -3728,6 +3734,10 @@ "description": "The ID of the settings flow", "type": "string", "format": "uuid" + }, + "url": { + "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", + "type": "string" } } }, @@ -3765,7 +3775,7 @@ "format": "uuid" }, "url": { - "description": "The URL of the verification flow", + "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" }, "verifiable_address": { From 0150795d902dcc7cfb2298c3b5a98da1c2541e46 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:11:37 +0200 Subject: [PATCH 140/262] feat(sdk): add missing profile discriminator to update registration --- .schema/openapi/patches/selfservice.yaml | 4 +- .../model_update_registration_flow_body.go | 44 ++++++++++++++++++- .../model_update_registration_flow_body.go | 44 ++++++++++++++++++- spec/api.json | 6 ++- 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index 81d82247586b..db9d7d3e6720 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -19,6 +19,7 @@ - "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod" - op: add path: /components/schemas/updateRegistrationFlowBody/discriminator value: @@ -28,7 +29,8 @@ oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" code: "#/components/schemas/updateRegistrationFlowWithCodeMethod" - passKey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + passkey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + profile: "#/components/schemas/updateRegistrationFlowWithProfileMethod" - op: add path: /components/schemas/registrationFlowState/enum value: diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go index 64374c620f8f..82a578cfc4d3 100644 --- a/internal/client-go/model_update_registration_flow_body.go +++ b/internal/client-go/model_update_registration_flow_body.go @@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd } } +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + // UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } - // check if the discriminator value is 'passKey' - if jsonDict["method"] == "passKey" { + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) if err == nil { @@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'webauthn' if jsonDict["method"] == "webauthn" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod' + if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } + if src.UpdateRegistrationFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod) + } + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) } @@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithPasswordMethod } + if obj.UpdateRegistrationFlowWithProfileMethod != nil { + return obj.UpdateRegistrationFlowWithProfileMethod + } + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { return obj.UpdateRegistrationFlowWithWebAuthnMethod } diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go index 64374c620f8f..82a578cfc4d3 100644 --- a/internal/httpclient/model_update_registration_flow_body.go +++ b/internal/httpclient/model_update_registration_flow_body.go @@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd } } +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + // UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } - // check if the discriminator value is 'passKey' - if jsonDict["method"] == "passKey" { + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) if err == nil { @@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'webauthn' if jsonDict["method"] == "webauthn" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod' + if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } + if src.UpdateRegistrationFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod) + } + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) } @@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithPasswordMethod } + if obj.UpdateRegistrationFlowWithProfileMethod != nil { + return obj.UpdateRegistrationFlowWithProfileMethod + } + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { return obj.UpdateRegistrationFlowWithWebAuthnMethod } diff --git a/spec/api.json b/spec/api.json index a78fe5793d00..afbd72885470 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2963,8 +2963,9 @@ "mapping": { "code": "#/components/schemas/updateRegistrationFlowWithCodeMethod", "oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod", - "passKey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod", + "passkey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod", "password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod", + "profile": "#/components/schemas/updateRegistrationFlowWithProfileMethod", "webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" }, "propertyName": "method" @@ -2984,6 +2985,9 @@ }, { "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + }, + { + "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod" } ] }, From dd6e53d62f343a317edf403218b20599539218c6 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:37:28 +0200 Subject: [PATCH 141/262] feat(sdk): avoid eval with javascript triggers Using `OnLoadTrigger` and `OnClickTrigger` one can now map the trigger to the corresponding JavaScript function. For example, trigger `{"on_click_trigger":"oryWebAuthnRegistration"}` should be translated to `window.oryWebAuthnRegistration()`: ``` if (attrs.onClickTrigger) { window[attrs.onClickTrigger]() } ``` --- .../model_ui_node_input_attributes.go | 78 ++++++++++- .../model_ui_node_input_attributes.go | 78 ++++++++++- ...sswordless-case=passkey_button_exists.json | 4 +- ...resh_passwordless_credentials-browser.json | 3 +- ...=refresh_passwordless_credentials-spa.json | 3 +- ...device_is_shown_which_can_be_unlinked.json | 3 +- ...-case=one_activation_element_is_shown.json | 3 +- ...on-case=passkey_button_exists-browser.json | 3 +- ...ration-case=passkey_button_exists-spa.json | 3 +- selfservice/strategy/passkey/passkey_login.go | 125 +++++++++++++++++- .../strategy/passkey/passkey_registration.go | 9 +- .../strategy/passkey/passkey_settings.go | 5 +- ...oad_is_set_when_identity_has_webauthn.json | 40 +++--- ...ebauthn_login_is_invalid-type=browser.json | 3 +- ...if_webauthn_login_is_invalid-type=spa.json | 3 +- ...passwordless_enabled=false#01-browser.json | 40 +++--- ...als-passwordless_enabled=false#01-spa.json | 40 +++--- ...passwordless_enabled=false#02-browser.json | 40 +++--- ...als-passwordless_enabled=false#02-spa.json | 40 +++--- ...ls-passwordless_enabled=false-browser.json | 40 +++--- ...ntials-passwordless_enabled=false-spa.json | 40 +++--- ...-passwordless_enabled=true#01-browser.json | 40 +++--- ...ials-passwordless_enabled=true#01-spa.json | 40 +++--- ...-passwordless_enabled=true#02-browser.json | 40 +++--- ...ials-passwordless_enabled=true#02-spa.json | 40 +++--- ...als-passwordless_enabled=true-browser.json | 40 +++--- ...entials-passwordless_enabled=true-spa.json | 40 +++--- ...device_is_shown_which_can_be_unlinked.json | 28 ++-- ...ast_credential_available-type=browser.json | 3 - ...he_last_credential_available-type=spa.json | 3 - ...-case=one_activation_element_is_shown.json | 28 ++-- ...f_it_is_MFA_at_all_times-type=browser.json | 3 - ...al_if_it_is_MFA_at_all_times-type=spa.json | 3 - ...n-case=webauthn_button_exists-browser.json | 6 +- ...ation-case=webauthn_button_exists-spa.json | 6 +- selfservice/strategy/webauthn/login_test.go | 18 +-- .../strategy/webauthn/registration_test.go | 1 + .../strategy/webauthn/settings_test.go | 17 +-- spec/api.json | 30 ++++- spec/swagger.json | 30 ++++- ui/node/attributes.go | 15 +++ x/webauthnx/js/trigger.go | 22 +++ x/webauthnx/js/trigger_test.go | 14 ++ x/webauthnx/js/webauthn.js | 39 +++++- x/webauthnx/nodes.go | 10 +- 45 files changed, 764 insertions(+), 355 deletions(-) create mode 100644 x/webauthnx/js/trigger.go create mode 100644 x/webauthnx/js/trigger_test.go diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go index b373dda7ccfd..7056a308d651 100644 --- a/internal/client-go/model_ui_node_input_attributes.go +++ b/internal/client-go/model_ui_node_input_attributes.go @@ -26,10 +26,14 @@ type UiNodeInputAttributes struct { Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` - // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. + // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. Onclick *string `json:"onclick,omitempty"` - // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnclickTrigger *string `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. Onload *string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnloadTrigger *string `json:"onloadTrigger,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -229,6 +233,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclickTrigger() string { + if o == nil || o.OnclickTrigger == nil { + var ret string + return ret + } + return *o.OnclickTrigger +} + +// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) { + if o == nil || o.OnclickTrigger == nil { + return nil, false + } + return o.OnclickTrigger, true +} + +// HasOnclickTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclickTrigger() bool { + if o != nil && o.OnclickTrigger != nil { + return true + } + + return false +} + +// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field. +func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) { + o.OnclickTrigger = &v +} + // GetOnload returns the Onload field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetOnload() string { if o == nil || o.Onload == nil { @@ -261,6 +297,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) { o.Onload = &v } +// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnloadTrigger() string { + if o == nil || o.OnloadTrigger == nil { + var ret string + return ret + } + return *o.OnloadTrigger +} + +// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) { + if o == nil || o.OnloadTrigger == nil { + return nil, false + } + return o.OnloadTrigger, true +} + +// HasOnloadTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnloadTrigger() bool { + if o != nil && o.OnloadTrigger != nil { + return true + } + + return false +} + +// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field. +func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) { + o.OnloadTrigger = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -402,9 +470,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.OnclickTrigger != nil { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } if o.Onload != nil { toSerialize["onload"] = o.Onload } + if o.OnloadTrigger != nil { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go index b373dda7ccfd..7056a308d651 100644 --- a/internal/httpclient/model_ui_node_input_attributes.go +++ b/internal/httpclient/model_ui_node_input_attributes.go @@ -26,10 +26,14 @@ type UiNodeInputAttributes struct { Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` - // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. + // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. Onclick *string `json:"onclick,omitempty"` - // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnclickTrigger *string `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. Onload *string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnloadTrigger *string `json:"onloadTrigger,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -229,6 +233,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclickTrigger() string { + if o == nil || o.OnclickTrigger == nil { + var ret string + return ret + } + return *o.OnclickTrigger +} + +// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) { + if o == nil || o.OnclickTrigger == nil { + return nil, false + } + return o.OnclickTrigger, true +} + +// HasOnclickTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclickTrigger() bool { + if o != nil && o.OnclickTrigger != nil { + return true + } + + return false +} + +// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field. +func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) { + o.OnclickTrigger = &v +} + // GetOnload returns the Onload field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetOnload() string { if o == nil || o.Onload == nil { @@ -261,6 +297,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) { o.Onload = &v } +// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnloadTrigger() string { + if o == nil || o.OnloadTrigger == nil { + var ret string + return ret + } + return *o.OnloadTrigger +} + +// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) { + if o == nil || o.OnloadTrigger == nil { + return nil, false + } + return o.OnloadTrigger, true +} + +// HasOnloadTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnloadTrigger() bool { + if o != nil && o.OnloadTrigger != nil { + return true + } + + return false +} + +// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field. +func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) { + o.OnloadTrigger = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -402,9 +470,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.OnclickTrigger != nil { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } if o.Onload != nil { toSerialize["onload"] = o.Onload } + if o.OnloadTrigger != nil { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json index d2dd6567d240..33c3cd4a7c34 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -38,7 +38,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -54,7 +54,9 @@ "name": "passkey_login_trigger", "node_type": "input", "onclick": "window.__oryPasskeyLogin()", + "onclick_trigger": "oryPasskeyLogin", "onload": "window.__oryPasskeyLoginAutocompleteInit()", + "onload_trigger": "oryPasskeyLoginAutocompleteInit", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json index c331d4f4280f..9b6602d9064f 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -46,6 +46,7 @@ "name": "passkey_login_trigger", "node_type": "input", "onclick": "window.__oryPasskeyLogin()", + "onclick_trigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json index c331d4f4280f..9b6602d9064f 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -46,6 +46,7 @@ "name": "passkey_login_trigger", "node_type": "input", "onclick": "window.__oryPasskeyLogin()", + "onclick_trigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index f9032e39049d..10cdc52924d6 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -5,6 +5,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeySettingsRegistration()", + "onclick_trigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, @@ -109,7 +110,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 7e5c5b3d082b..ce4393561cfd 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -5,6 +5,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeySettingsRegistration()", + "onclick_trigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, @@ -61,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json index 18e0cda77811..4eb8c3d30eb7 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,6 +71,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeyRegistration()", + "onclick_trigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json index 18e0cda77811..4eb8c3d30eb7 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,6 +71,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeyRegistration()", + "onclick_trigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index 54b3f475ed38..927944261007 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -9,6 +9,8 @@ import ( "net/http" "strings" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/pkg/errors" @@ -103,6 +105,119 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo Type: node.InputAttributeTypeHidden, }}) + loginFlow.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *login.Flow) error { + ctx := r.Context() + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, loginFlow, s.ID()) + if identifier == "" { + return nil + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + if err != nil { + return err + } + + cred, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no passkey + return nil + } + + var conf identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return errors.WithStack(err) + } + + webAuthCreds := conf.Credentials.ToWebAuthn() + if len(webAuthCreds) == 0 { + // Identity has no webauthn + return nil + } + + passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ + Name: passkeyIdentifier, + ID: conf.UserHandle, + Credentials: webAuthCreds, + Config: webAuthn.Config, + }) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) + } + + loginFlow.InternalContext, err = sjson.SetBytes( + loginFlow.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + loginFlow.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + }}) + + loginFlow.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + loginFlow.UI.SetNode(node.NewInputField( + "identifier", + passkeyIdentifier, + node.DefaultGroup, + node.InputAttributeTypeHidden, + )) + return nil } @@ -362,7 +477,8 @@ func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, f *login.Flow) er node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) @@ -392,8 +508,11 @@ func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flo node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + + attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here + attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) diff --git a/selfservice/strategy/passkey/passkey_registration.go b/selfservice/strategy/passkey/passkey_registration.go index 88efd420d725..9be753f70c40 100644 --- a/selfservice/strategy/passkey/passkey_registration.go +++ b/selfservice/strategy/passkey/passkey_registration.go @@ -11,6 +11,8 @@ import ( "net/url" "strings" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/pkg/errors" @@ -280,9 +282,10 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registra Group: node.PasskeyGroup, Meta: &node.Meta{Label: text.NewInfoSelfServiceRegistrationRegisterPasskey()}, Attributes: &node.InputAttributes{ - Name: node.PasskeyRegisterTrigger, - Type: node.InputAttributeTypeButton, - OnClick: "window.__oryPasskeyRegistration()", // defined in webauthn.js + Name: node.PasskeyRegisterTrigger, + Type: node.InputAttributeTypeButton, + OnClick: js.WebAuthnTriggersPasskeyRegistration.String() + "()", // defined in webauthn.js + OnClickTrigger: js.WebAuthnTriggersPasskeyRegistration, }}) // Passkey nodes end diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go index 548a261e442b..04beff02ab09 100644 --- a/selfservice/strategy/passkey/passkey_settings.go +++ b/selfservice/strategy/passkey/passkey_settings.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/gofrs/uuid" @@ -114,7 +116,8 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryPasskeySettingsRegistration()" + a.OnClick = js.WebAuthnTriggersPasskeySettingsRegistration.String() + "()" + a.OnClickTrigger = js.WebAuthnTriggersPasskeySettingsRegistration }), ).WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterPasskey())) diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index ca960c98d683..472dc71f4672 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -24,25 +24,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -61,7 +42,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -70,5 +51,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index f4be195cdecf..03a66e9c5616 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "type": "text/javascript", "node_type": "script" }, @@ -51,6 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, + "onclick_trigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index f4be195cdecf..03a66e9c5616 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "type": "text/javascript", "node_type": "script" }, @@ -51,6 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, + "onclick_trigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 0b1702c09413..02acdfb345d5 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -82,33 +82,33 @@ { "attributes": { "disabled": false, - "name": "webauthn_register_trigger", + "name": "webauthn_register", "node_type": "input", - "type": "button", + "type": "hidden", "value": "" }, "group": "webauthn", "messages": [], - "meta": { - "label": { - "id": 1050012, - "text": "Add security key", - "type": "info" - } - }, + "meta": {}, "type": "input" }, { "attributes": { "disabled": false, - "name": "webauthn_register", + "name": "webauthn_register_trigger", "node_type": "input", - "type": "hidden", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], - "meta": {}, + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, "type": "input" }, { @@ -116,7 +116,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index b21fa4833028..8fddd27469f3 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -34,33 +34,33 @@ { "attributes": { "disabled": false, - "name": "webauthn_register_trigger", + "name": "webauthn_register", "node_type": "input", - "type": "button", + "type": "hidden", "value": "" }, "group": "webauthn", "messages": [], - "meta": { - "label": { - "id": 1050012, - "text": "Add security key", - "type": "info" - } - }, + "meta": {}, "type": "input" }, { "attributes": { "disabled": false, - "name": "webauthn_register", + "name": "webauthn_register_trigger", "node_type": "input", - "type": "hidden", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], - "meta": {}, + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, "type": "input" }, { @@ -68,7 +68,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index 14a920d0a18d..edcf3a92b509 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -75,8 +75,8 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "type": "button", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index 14a920d0a18d..edcf3a92b509 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -75,8 +75,8 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "type": "button", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go index 46db972c4cc7..cfb2b18455ac 100644 --- a/selfservice/strategy/webauthn/login_test.go +++ b/selfservice/strategy/webauthn/login_test.go @@ -170,9 +170,10 @@ func TestCompleteLogin(t *testing.T) { }, testhelpers.InitFlowWithRefresh()) snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "2.attributes.onclick", - "4.attributes.nonce", - "4.attributes.src", + "3.attributes.nonce", + "3.attributes.src", + "4.attributes.value", + "4.attributes.onclick", }) nodes, err := json.Marshal(f.Ui.Nodes) require.NoError(t, err) @@ -475,12 +476,13 @@ func TestCompleteLogin(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", "1.attributes.value", - "2.attributes.onclick", - "2.attributes.onload", - "4.attributes.src", - "4.attributes.nonce", + "3.attributes.src", + "3.attributes.nonce", + "4.attributes.onclick", + "4.attributes.onload", + "4.attributes.value", }) - ensureReplacement(t, "2", f.Ui, "allowCredentials") + ensureReplacement(t, "4", f.Ui, "allowCredentials") }) t.Run("case=webauthn payload is not set when identity has no webauthn", func(t *testing.T) { diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index 973e1ae0ec81..8dd3e38bd036 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -145,6 +145,7 @@ func TestRegistration(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "2.attributes.value", "5.attributes.onclick", + "5.attributes.value", "6.attributes.nonce", "6.attributes.src", }) diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index 3b46cc8de752..9a34159c9a3d 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -143,11 +143,12 @@ func TestCompleteSettings(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "4.attributes.onclick", + "5.attributes.onclick", + "5.attributes.value", "6.attributes.src", "6.attributes.nonce", }) - ensureReplacement(t, "4", f.Ui, "Ory Corp") + ensureReplacement(t, "5", f.Ui, "Ory Corp") }) t.Run("case=one activation element is shown", func(t *testing.T) { @@ -159,12 +160,13 @@ func TestCompleteSettings(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "2.attributes.onload", - "2.attributes.onclick", + "3.attributes.onload", + "3.attributes.onclick", + "3.attributes.value", "4.attributes.src", "4.attributes.nonce", }) - ensureReplacement(t, "2", f.Ui, "Ory Corp") + ensureReplacement(t, "3", f.Ui, "Ory Corp") }) t.Run("case=webauthn only works for browsers", func(t *testing.T) { @@ -375,7 +377,7 @@ func TestCompleteSettings(t *testing.T) { body, res := doBrowserFlow(t, spa, func(v url.Values) { // The remove key should be empty - snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"}) + snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"}) v.Set(node.WebAuthnRemove, "666f6f666f6f") }, id) @@ -416,7 +418,7 @@ func TestCompleteSettings(t *testing.T) { body, res := doBrowserFlow(t, spa, func(v url.Values) { // The remove key should be set - snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"}) + snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"}) v.Set(node.WebAuthnRemove, "666f6f666f6f") }, id) @@ -481,7 +483,6 @@ func TestCompleteSettings(t *testing.T) { // Check not to remove other credentials with webauthn _, ok = actual.GetCredentials(identity.CredentialsTypePassword) assert.True(t, ok) - } t.Run("type=browser", func(t *testing.T) { diff --git a/spec/api.json b/spec/api.json index afbd72885470..60a4c8b539c8 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2396,13 +2396,39 @@ "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { - "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", + "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.", "type": "string" }, + "onclickTrigger": { + "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "type": "string", + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "onload": { - "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.", + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.", "type": "string" }, + "onloadTrigger": { + "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "type": "string", + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "pattern": { "description": "The input's pattern.", "type": "string" diff --git a/spec/swagger.json b/spec/swagger.json index fe16afa03c25..9f4636b75977 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5477,13 +5477,39 @@ "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { - "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", + "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.", "type": "string" }, + "onclickTrigger": { + "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "type": "string", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "onload": { - "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.", + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.", "type": "string" }, + "onloadTrigger": { + "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "type": "string", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "pattern": { "description": "The input's pattern.", "type": "string" diff --git a/ui/node/attributes.go b/ui/node/attributes.go index 762df9fd46c7..2c8045ecb743 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -6,6 +6,7 @@ package node import ( "fmt" "github.com/ory/kratos/text" + "github.com/ory/kratos/x/webauthnx/js" ) const ( @@ -97,12 +98,26 @@ type InputAttributes struct { // OnClick may contain javascript which should be executed on click. This is primarily // used for WebAuthn. + // + // Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. OnClick string `json:"onclick,omitempty"` + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. + // + // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. + OnClickTrigger js.WebAuthnTriggers `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily // used for WebAuthn. + // + // Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. OnLoad string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. + // + // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. + OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"` + // NodeType represents this node's types. It is a mirror of `node.type` and // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input". // diff --git a/x/webauthnx/js/trigger.go b/x/webauthnx/js/trigger.go new file mode 100644 index 000000000000..7b236191ce8e --- /dev/null +++ b/x/webauthnx/js/trigger.go @@ -0,0 +1,22 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package js + +import "fmt" + +// swagger:enum WebAuthnTriggers +type WebAuthnTriggers string + +const ( + WebAuthnTriggersWebAuthnRegistration WebAuthnTriggers = "oryWebAuthnRegistration" + WebAuthnTriggersWebAuthnLogin WebAuthnTriggers = "oryWebAuthnLogin" + WebAuthnTriggersPasskeyLogin WebAuthnTriggers = "oryPasskeyLogin" + WebAuthnTriggersPasskeyLoginAutocompleteInit WebAuthnTriggers = "oryPasskeyLoginAutocompleteInit" + WebAuthnTriggersPasskeyRegistration WebAuthnTriggers = "oryPasskeyRegistration" + WebAuthnTriggersPasskeySettingsRegistration WebAuthnTriggers = "oryPasskeySettingsRegistration" +) + +func (r WebAuthnTriggers) String() string { + return fmt.Sprintf("window.%s", string(r)) +} diff --git a/x/webauthnx/js/trigger_test.go b/x/webauthnx/js/trigger_test.go new file mode 100644 index 000000000000..97f9dc00ee77 --- /dev/null +++ b/x/webauthnx/js/trigger_test.go @@ -0,0 +1,14 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package js + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToString(t *testing.T) { + assert.Equal(t, "window.oryWebAuthnRegistration", WebAuthnTriggersWebAuthnRegistration.String()) +} diff --git a/x/webauthnx/js/webauthn.js b/x/webauthnx/js/webauthn.js index 61a7cb8f976d..638bd4ece082 100644 --- a/x/webauthnx/js/webauthn.js +++ b/x/webauthnx/js/webauthn.js @@ -32,7 +32,7 @@ } function __oryWebAuthnLogin( - opt, + options, resultQuerySelector = '*[name="webauthn_login"]', triggerQuerySelector = '*[name="webauthn_login_trigger"]', ) { @@ -40,6 +40,12 @@ alert("This browser does not support WebAuthn!") } + const triggerEl = document.querySelector(triggerQuerySelector) + let opt = options + if (!opt) { + opt = JSON.parse(triggerEl.value) + } + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( function (value) { @@ -71,7 +77,7 @@ }, }) - document.querySelector(triggerQuerySelector).closest("form").submit() + triggerEl.closest("form").submit() }) .catch((err) => { alert(err) @@ -79,7 +85,7 @@ } function __oryWebAuthnRegistration( - opt, + options, resultQuerySelector = '*[name="webauthn_register"]', triggerQuerySelector = '*[name="webauthn_register_trigger"]', ) { @@ -87,6 +93,12 @@ alert("This browser does not support WebAuthn!") } + const triggerEl = document.querySelector(triggerQuerySelector) + let opt = options + if (!opt) { + opt = JSON.parse(triggerEl.value) + } + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) @@ -118,14 +130,14 @@ }, }) - document.querySelector(triggerQuerySelector).closest("form").submit() + triggerEl.closest("form").submit() }) .catch((err) => { alert(err) }) } - window.__oryPasskeyLoginAutocompleteInit = async function () { + async function __oryPasskeyLoginAutocompleteInit () { const dataEl = document.getElementsByName("passkey_challenge")[0] const resultEl = document.getElementsByName("passkey_login")[0] const identifierEl = document.getElementsByName("identifier")[0] @@ -195,7 +207,7 @@ }) } - window.__oryPasskeyLogin = function () { + function __oryPasskeyLogin () { const dataEl = document.getElementsByName("passkey_challenge")[0] const resultEl = document.getElementsByName("passkey_login")[0] @@ -262,7 +274,7 @@ }) } - window.__oryPasskeyRegistration = function () { + function __oryPasskeyRegistration () { const dataEl = document.getElementsByName("passkey_create_data")[0] const resultEl = document.getElementsByName("passkey_register")[0] @@ -373,8 +385,21 @@ }) } + // Deprecated naming with underscores - kept for support with Ory Elements v0 window.__oryWebAuthnLogin = __oryWebAuthnLogin window.__oryWebAuthnRegistration = __oryWebAuthnRegistration window.__oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration + window.__oryPasskeyLogin = __oryPasskeyLogin + window.__oryPasskeyRegistration = __oryPasskeyRegistration + window.__oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit + + // Current naming - use with Ory Elements v1 + window.oryWebAuthnLogin = __oryWebAuthnLogin + window.oryWebAuthnRegistration = __oryWebAuthnRegistration + window.oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration + window.oryPasskeyLogin = __oryPasskeyLogin + window.oryPasskeyRegistration = __oryPasskeyRegistration + window.oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit + window.__oryWebAuthnInitialized = true })() diff --git a/x/webauthnx/nodes.go b/x/webauthnx/nodes.go index 76fac1c397cd..85ecf621cfda 100644 --- a/x/webauthnx/nodes.go +++ b/x/webauthnx/nodes.go @@ -10,6 +10,8 @@ import ( "fmt" "net/url" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/ory/x/stringsx" "github.com/ory/x/urlx" @@ -21,7 +23,9 @@ import ( func NewWebAuthnConnectionTrigger(options string) *node.Node { return node.NewInputField(node.WebAuthnRegisterTrigger, "", node.WebAuthnGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryWebAuthnRegistration(" + options + ")" + a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnRegistration, options) + a.OnClickTrigger = js.WebAuthnTriggersWebAuthnRegistration + a.FieldValue = options })) } @@ -44,7 +48,9 @@ func NewWebAuthnConnectionInput() *node.Node { func NewWebAuthnLoginTrigger(options string) *node.Node { return node.NewInputField(node.WebAuthnLoginTrigger, "", node.WebAuthnGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryWebAuthnLogin(" + options + ")" + a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnLogin, options) + a.FieldValue = options + a.OnClickTrigger = js.WebAuthnTriggersWebAuthnLogin })) } From 04850f45cfbdc89223366ffa3b540d579a3b44be Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:49:23 +0200 Subject: [PATCH 142/262] fix: replace submit with continue button for recovery and verification and add maxlength --- .../model_ui_node_input_attributes.go | 37 +++++++++++++++++++ .../model_ui_node_input_attributes.go | 37 +++++++++++++++++++ ...=fails_if_active_strategy_is_disabled.json | 4 +- ...=fails_if_active_strategy_is_disabled.json | 4 +- ...=fails_if_active_strategy_is_disabled.json | 4 +- ...=fails_if_active_strategy_is_disabled.json | 4 +- ...ail_field_when_creating_recovery_code.json | 4 +- ...set_all_the_correct_recovery_payloads.json | 4 +- ...ct_recovery_payloads_after_submission.json | 4 +- ...he_correct_recovery_payloads-type=api.json | 4 +- ...orrect_recovery_payloads-type=browser.json | 4 +- ...he_correct_recovery_payloads-type=spa.json | 4 +- ...ry_payloads_after_submission-type=api.json | 4 +- ...ayloads_after_submission-type=browser.json | 4 +- ...ry_payloads_after_submission-type=spa.json | 4 +- ...all_the_correct_verification_payloads.json | 4 +- ...erification_payloads_after_submission.json | 4 +- selfservice/strategy/code/strategy.go | 9 +++-- .../strategy/code/strategy_recovery.go | 5 ++- .../strategy/code/strategy_recovery_admin.go | 7 +++- ...set_all_the_correct_recovery_payloads.json | 4 +- ...ct_recovery_payloads_after_submission.json | 4 +- ...all_the_correct_verification_payloads.json | 4 +- ...erification_payloads_after_submission.json | 4 +- .../strategy/link/strategy_recovery.go | 2 +- .../strategy/link/strategy_verification.go | 2 +- ...sswordless-case=passkey_button_exists.json | 8 ++-- ...resh_passwordless_credentials-browser.json | 4 +- ...=refresh_passwordless_credentials-spa.json | 4 +- ...device_is_shown_which_can_be_unlinked.json | 4 +- ...-case=one_activation_element_is_shown.json | 4 +- ...on-case=passkey_button_exists-browser.json | 4 +- ...ration-case=passkey_button_exists-spa.json | 4 +- ...oad_is_set_when_identity_has_webauthn.json | 2 +- ...ebauthn_login_is_invalid-type=browser.json | 2 +- ...if_webauthn_login_is_invalid-type=spa.json | 2 +- ...passwordless_enabled=false#01-browser.json | 2 +- ...als-passwordless_enabled=false#01-spa.json | 2 +- ...passwordless_enabled=false#02-browser.json | 2 +- ...als-passwordless_enabled=false#02-spa.json | 2 +- ...ls-passwordless_enabled=false-browser.json | 2 +- ...ntials-passwordless_enabled=false-spa.json | 2 +- ...-passwordless_enabled=true#01-browser.json | 2 +- ...ials-passwordless_enabled=true#01-spa.json | 2 +- ...-passwordless_enabled=true#02-browser.json | 2 +- ...ials-passwordless_enabled=true#02-spa.json | 2 +- ...als-passwordless_enabled=true-browser.json | 2 +- ...entials-passwordless_enabled=true-spa.json | 2 +- ...device_is_shown_which_can_be_unlinked.json | 2 +- ...-case=one_activation_element_is_shown.json | 2 +- ...n-case=webauthn_button_exists-browser.json | 2 +- ...ation-case=webauthn_button_exists-spa.json | 2 +- spec/api.json | 5 +++ spec/swagger.json | 5 +++ ui/node/attributes.go | 3 ++ 55 files changed, 176 insertions(+), 82 deletions(-) diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go index 7056a308d651..f8deff5d5417 100644 --- a/internal/client-go/model_ui_node_input_attributes.go +++ b/internal/client-go/model_ui_node_input_attributes.go @@ -22,6 +22,8 @@ type UiNodeInputAttributes struct { // Sets the input's disabled field to true or false. Disabled bool `json:"disabled"` Label *UiText `json:"label,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,omitempty"` // The input's element name. Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script @@ -153,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) { o.Label = &v } +// GetMaxlength returns the Maxlength field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetMaxlength() int64 { + if o == nil || o.Maxlength == nil { + var ret int64 + return ret + } + return *o.Maxlength +} + +// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) { + if o == nil || o.Maxlength == nil { + return nil, false + } + return o.Maxlength, true +} + +// HasMaxlength returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasMaxlength() bool { + if o != nil && o.Maxlength != nil { + return true + } + + return false +} + +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { + o.Maxlength = &v +} + // GetName returns the Name field value func (o *UiNodeInputAttributes) GetName() string { if o == nil { @@ -461,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Label != nil { toSerialize["label"] = o.Label } + if o.Maxlength != nil { + toSerialize["maxlength"] = o.Maxlength + } if true { toSerialize["name"] = o.Name } diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go index 7056a308d651..f8deff5d5417 100644 --- a/internal/httpclient/model_ui_node_input_attributes.go +++ b/internal/httpclient/model_ui_node_input_attributes.go @@ -22,6 +22,8 @@ type UiNodeInputAttributes struct { // Sets the input's disabled field to true or false. Disabled bool `json:"disabled"` Label *UiText `json:"label,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,omitempty"` // The input's element name. Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script @@ -153,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) { o.Label = &v } +// GetMaxlength returns the Maxlength field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetMaxlength() int64 { + if o == nil || o.Maxlength == nil { + var ret int64 + return ret + } + return *o.Maxlength +} + +// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) { + if o == nil || o.Maxlength == nil { + return nil, false + } + return o.Maxlength, true +} + +// HasMaxlength returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasMaxlength() bool { + if o != nil && o.Maxlength != nil { + return true + } + + return false +} + +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { + o.Maxlength = &v +} + // GetName returns the Name field value func (o *UiNodeInputAttributes) GetName() string { if o == nil { @@ -461,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Label != nil { toSerialize["label"] = o.Label } + if o.Maxlength != nil { + toSerialize["maxlength"] = o.Maxlength + } if true { toSerialize["name"] = o.Name } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json index f4c0270da2dc..17eb6e965bcb 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json index 56782eed4571..a9ad1e527fb4 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json index f4c0270da2dc..17eb6e965bcb 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json index 56782eed4571..a9ad1e527fb4 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json index a9f46bedb5c4..7030380e7fc2 100644 --- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json @@ -31,8 +31,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json index 37f61ac9e827..01def57fd58f 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -30,8 +30,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 42456da54dc5..7e7096cd7358 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -44,8 +44,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index 80147786477a..351949f7cd95 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -193,7 +193,7 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeInputEmail()), ) - codeMetaLabel = text.NewInfoNodeLabelSubmit() + codeMetaLabel = text.NewInfoNodeLabelContinue() case *login.Flow: ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(ctx) if err != nil { @@ -363,13 +363,16 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error ) // code input field - freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "[0-9]+" + a.MaxLength = CodeLength + })). WithMetaLabel(codeMetaLabel)) // code submit button freshNodes. Append(node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) if resendNode != nil { freshNodes.Append(resendNode) diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 9376a8e1ef4d..f33356f2df31 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -43,7 +43,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err f.UI. GetNodes(). Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } @@ -406,6 +406,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithInputAttributes(func(a *node.InputAttributes) { a.Required = true a.Pattern = "[0-9]+" + a.MaxLength = CodeLength })). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) @@ -414,7 +415,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI. GetNodes(). Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) f.UI.Nodes.Append(node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeResendOTP()), diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go index 028bb811bcaa..63aa36a90edd 100644 --- a/selfservice/strategy/code/strategy_recovery_admin.go +++ b/selfservice/strategy/code/strategy_recovery_admin.go @@ -178,13 +178,16 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. recoveryFlow.DangerousSkipCSRFCheck = true recoveryFlow.State = flow.StateEmailSent recoveryFlow.UI.Nodes = node.Nodes{} - recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "[0-9]+" + a.MaxLength = CodeLength + })). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) recoveryFlow.UI.Nodes. Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil { s.deps.Writer().WriteError(w, r, err) diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json index 3bb3cbbf3ef6..5ac9946936c8 100644 --- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index 498575cfee1b..1a8d048fe37d 100644 --- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -45,8 +45,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json index 3bb3cbbf3ef6..5ac9946936c8 100644 --- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 498575cfee1b..1a8d048fe37d 100644 --- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -45,8 +45,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 184399ca1002..e6d91051c2c4 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -56,7 +56,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err // v0.5: form.Field{Name: "email", Type: "email", Required: true}, node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit())) + f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index a2a72ea9a277..61f95da52fef 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -44,7 +44,7 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F // v0.5: form.Field{Name: "email", Type: "email", Required: true} node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit())) + f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json index 33c3cd4a7c34..0635ea89a614 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -53,10 +53,10 @@ "disabled": false, "name": "passkey_login_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyLogin()", - "onclick_trigger": "oryPasskeyLogin", - "onload": "window.__oryPasskeyLoginAutocompleteInit()", - "onload_trigger": "oryPasskeyLoginAutocompleteInit", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "onload": "window.oryPasskeyLoginAutocompleteInit()", + "onloadTrigger": "oryPasskeyLoginAutocompleteInit", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json index 9b6602d9064f..c9ece0d3c08e 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -45,8 +45,8 @@ "disabled": false, "name": "passkey_login_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyLogin()", - "onclick_trigger": "oryPasskeyLogin", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json index 9b6602d9064f..c9ece0d3c08e 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -45,8 +45,8 @@ "disabled": false, "name": "passkey_login_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyLogin()", - "onclick_trigger": "oryPasskeyLogin", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 10cdc52924d6..b7e2168a1591 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -4,8 +4,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeySettingsRegistration()", - "onclick_trigger": "oryPasskeySettingsRegistration", + "onclick": "window.oryPasskeySettingsRegistration()", + "onclickTrigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index ce4393561cfd..88861c80cdcb 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -4,8 +4,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeySettingsRegistration()", - "onclick_trigger": "oryPasskeySettingsRegistration", + "onclick": "window.oryPasskeySettingsRegistration()", + "onclickTrigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json index 4eb8c3d30eb7..e232b6edde24 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -70,8 +70,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyRegistration()", - "onclick_trigger": "oryPasskeyRegistration", + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json index 4eb8c3d30eb7..e232b6edde24 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -70,8 +70,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyRegistration()", - "onclick_trigger": "oryPasskeyRegistration", + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index 472dc71f4672..71ffeaabc0d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -57,7 +57,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index 03a66e9c5616..77f06d2f6c1e 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -51,7 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index 03a66e9c5616..77f06d2f6c1e 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -51,7 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 02acdfb345d5..7fab410d6716 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -97,7 +97,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 8fddd27469f3..68bfeefda8cc 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -49,7 +49,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index edcf3a92b509..91b3ff4cfcbc 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -75,7 +75,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index edcf3a92b509..91b3ff4cfcbc 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -75,7 +75,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/spec/api.json b/spec/api.json index 60a4c8b539c8..e76a0b1737f1 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2379,6 +2379,11 @@ "label": { "$ref": "#/components/schemas/uiText" }, + "maxlength": { + "description": "MaxLength may contain the input's maximum length.", + "format": "int64", + "type": "integer" + }, "name": { "description": "The input's element name.", "type": "string" diff --git a/spec/swagger.json b/spec/swagger.json index 9f4636b75977..5cab5b7aea54 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5460,6 +5460,11 @@ "label": { "$ref": "#/definitions/uiText" }, + "maxlength": { + "description": "MaxLength may contain the input's maximum length.", + "type": "integer", + "format": "int64" + }, "name": { "description": "The input's element name.", "type": "string" diff --git a/ui/node/attributes.go b/ui/node/attributes.go index 2c8045ecb743..7db1927c3499 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -118,6 +118,9 @@ type InputAttributes struct { // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"` + // MaxLength may contain the input's maximum length. + MaxLength int `json:"maxlength,omitempty"` + // NodeType represents this node's types. It is a mirror of `node.type` and // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input". // From 51042d99fab301f0bb44665e56c5a2364e7d8866 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:43:41 +0200 Subject: [PATCH 143/262] feat: set maxlength for totp input --- ...not_contain_email_field_when_creating_recovery_code.json | 2 ++ ..._all_the_correct_recovery_payloads_after_submission.json | 1 + ...correct_recovery_payloads_after_submission-type=api.json | 1 + ...ect_recovery_payloads_after_submission-type=browser.json | 1 + ...correct_recovery_payloads_after_submission-type=spa.json | 1 + ..._the_correct_verification_payloads_after_submission.json | 2 ++ ...eLogin-flow=passwordless-case=passkey_button_exists.json | 2 +- ...fresh-case=refresh_passwordless_credentials-browser.json | 2 +- ...w=refresh-case=refresh_passwordless_credentials-spa.json | 2 +- ...ttings-case=a_device_is_shown_which_can_be_unlinked.json | 2 +- ...mpleteSettings-case=one_activation_element_is_shown.json | 2 +- ...TestRegistration-case=passkey_button_exists-browser.json | 2 +- .../TestRegistration-case=passkey_button_exists-spa.json | 2 +- ...gin-case=totp_payload_is_set_when_identity_has_totp.json | 1 + selfservice/strategy/totp/generator.go | 3 ++- selfservice/strategy/totp/login.go | 2 +- ...=webauthn_payload_is_set_when_identity_has_webauthn.json | 2 +- ...ould_fail_if_webauthn_login_is_invalid-type=browser.json | 2 +- ...e=should_fail_if_webauthn_login_is_invalid-type=spa.json | 2 +- ...0_credentials-passwordless_enabled=false#01-browser.json | 2 +- ...fa_v0_credentials-passwordless_enabled=false#01-spa.json | 2 +- ...0_credentials-passwordless_enabled=false#02-browser.json | 2 +- ...fa_v0_credentials-passwordless_enabled=false#02-spa.json | 2 +- ...a_v0_credentials-passwordless_enabled=false-browser.json | 2 +- ...e=mfa_v0_credentials-passwordless_enabled=false-spa.json | 2 +- ...v0_credentials-passwordless_enabled=true#01-browser.json | 2 +- ...mfa_v0_credentials-passwordless_enabled=true#01-spa.json | 2 +- ...v0_credentials-passwordless_enabled=true#02-browser.json | 2 +- ...mfa_v0_credentials-passwordless_enabled=true#02-spa.json | 2 +- ...fa_v0_credentials-passwordless_enabled=true-browser.json | 2 +- ...se=mfa_v0_credentials-passwordless_enabled=true-spa.json | 2 +- ...ttings-case=a_device_is_shown_which_can_be_unlinked.json | 2 +- ...mpleteSettings-case=one_activation_element_is_shown.json | 2 +- ...estRegistration-case=webauthn_button_exists-browser.json | 2 +- .../TestRegistration-case=webauthn_button_exists-spa.json | 2 +- ui/node/attributes_input.go | 6 ++++++ 36 files changed, 44 insertions(+), 28 deletions(-) diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json index 7030380e7fc2..736578d0e543 100644 --- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json @@ -6,7 +6,9 @@ "name": "code", "type": "text", "required": true, + "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 7e7096cd7358..fde2aae2986f 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -19,7 +19,9 @@ "name": "code", "type": "text", "required": true, + "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json index 0635ea89a614..e99dd52e418e 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -38,7 +38,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json index c9ece0d3c08e..1e026fb9979a 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json index c9ece0d3c08e..1e026fb9979a 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index b7e2168a1591..a0383567eda4 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -110,7 +110,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 88861c80cdcb..8d91edf04ce5 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json index e232b6edde24..e4c5160c9697 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json index e232b6edde24..e4c5160c9697 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json index afae3de49f05..23611d1c2255 100644 --- a/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json +++ b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json @@ -15,6 +15,7 @@ { "attributes": { "disabled": false, + "maxlength": 6, "name": "totp_code", "node_type": "input", "required": true, diff --git a/selfservice/strategy/totp/generator.go b/selfservice/strategy/totp/generator.go index fe79d8991d0d..9846506f1671 100644 --- a/selfservice/strategy/totp/generator.go +++ b/selfservice/strategy/totp/generator.go @@ -25,6 +25,7 @@ import ( // So we need 160/8 = 20 key length. stdtotp.Generate uses the key // length for reading from crypto.Rand. const secretSize = 160 / 8 +const digits = otp.DigitsSix func NewKey(ctx context.Context, accountName string, d interface { config.Provider @@ -33,7 +34,7 @@ func NewKey(ctx context.Context, accountName string, d interface { Issuer: d.Config().TOTPIssuer(ctx), AccountName: accountName, SecretSize: secretSize, - Digits: otp.DigitsSix, + Digits: digits, Period: 30, }) if err != nil { diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go index 2aaface8dc5c..7b4564cb165d 100644 --- a/selfservice/strategy/totp/login.go +++ b/selfservice/strategy/totp/login.go @@ -50,7 +50,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("totp_code", "", node.TOTPGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoLoginTOTPLabel())) + sr.UI.SetNode(node.NewInputField("totp_code", "", node.TOTPGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithMaxLengthInputAttribute(int(digits))).WithMetaLabel(text.NewInfoLoginTOTPLabel())) sr.UI.GetNodes().Append(node.NewInputField("method", s.ID(), node.TOTPGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginTOTP())) return nil diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index 71ffeaabc0d0..71fbb382f0de 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -42,7 +42,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index 77f06d2f6c1e..399562e7015d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index 77f06d2f6c1e..399562e7015d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 7fab410d6716..f0edfe3c5966 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -116,7 +116,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 68bfeefda8cc..c15a847d4703 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -68,7 +68,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index 91b3ff4cfcbc..20e3d3566fb0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index 91b3ff4cfcbc..20e3d3566fb0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go index b63ac9365a51..176c1a25b42e 100644 --- a/ui/node/attributes_input.go +++ b/ui/node/attributes_input.go @@ -38,6 +38,12 @@ func WithRequiredInputAttribute(a *InputAttributes) { a.Required = true } +func WithMaxLengthInputAttribute(maxLength int) func(a *InputAttributes) { + return func(a *InputAttributes) { + a.MaxLength = maxLength + } +} + func WithInputAttributes(f func(a *InputAttributes)) func(a *InputAttributes) { return func(a *InputAttributes) { f(a) From 7597bc6345848b66161d5a9b7a42307bbc85c978 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 24 May 2024 10:38:20 +0200 Subject: [PATCH 144/262] fix: add missing JS triggers --- selfservice/strategy/passkey/passkey_login.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index 927944261007..f7d2d4580ed4 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -111,8 +111,11 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + + attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here + attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) @@ -206,7 +209,8 @@ func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *log node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) @@ -549,8 +553,11 @@ func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *lo node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + + attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here + attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) From 612e3bf09dbffd3feba08d5100bffbc39cbd240a Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 24 May 2024 10:41:48 +0200 Subject: [PATCH 145/262] feat: add if method to sdk --- .schema/openapi/patches/selfservice.yaml | 2 ++ selfservice/strategy/multistep/strategy_login.go | 4 ++-- selfservice/strategy/multistep/types.go | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index db9d7d3e6720..88aca38afef4 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -52,6 +52,7 @@ - "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" - "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" - "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithTwoStepMethod" - op: add path: /components/schemas/updateLoginFlowBody/discriminator value: @@ -64,6 +65,7 @@ lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod" code: "#/components/schemas/updateLoginFlowWithCodeMethod" passkey: "#/components/schemas/updateLoginFlowWithPasskeyMethod" + two_step: "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod" - op: add path: /components/schemas/loginFlowState/enum value: diff --git a/selfservice/strategy/multistep/strategy_login.go b/selfservice/strategy/multistep/strategy_login.go index 3552c5f11095..c4b821163ad4 100644 --- a/selfservice/strategy/multistep/strategy_login.go +++ b/selfservice/strategy/multistep/strategy_login.go @@ -18,7 +18,7 @@ import ( var _ login.FormHydrator = new(Strategy) var _ login.Strategy = new(Strategy) -func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithMultiStepMethod, err error) error { +func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithIdentifierFirstMethod, err error) error { if f != nil { f.UI.Nodes.SetValueAttribute("identifier", payload.Identifier) if f.Type == flow.TypeBrowser { @@ -38,7 +38,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - var p updateLoginFlowWithMultiStepMethod + var p updateLoginFlowWithIdentifierFirstMethod if err := s.hd.Decode(r, &p, decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), diff --git a/selfservice/strategy/multistep/types.go b/selfservice/strategy/multistep/types.go index 5268f9c49195..1ac62c5c0667 100644 --- a/selfservice/strategy/multistep/types.go +++ b/selfservice/strategy/multistep/types.go @@ -4,8 +4,8 @@ import "encoding/json" // Update Login Flow with Multi-Step Method // -// swagger:model updateLoginFlowWithMultiStepMethod -type updateLoginFlowWithMultiStepMethod struct { +// swagger:model updateLoginFlowWithIdentifierFirstMethod +type updateLoginFlowWithIdentifierFirstMethod struct { // Method should be set to "password" when logging in using the identifier and password strategy. // // required: true From 5d8e3276036841fb01fafe8f3c9dd44bc21ee51f Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 24 May 2024 11:12:57 +0200 Subject: [PATCH 146/262] chore: regenerate SDK --- cmd/cliclient/client.go | 2 +- cmd/identities/definitions.go | 2 +- cmd/identities/get.go | 2 +- cmd/identities/import.go | 2 +- cmd/identities/import_test.go | 2 +- examples/go/selfservice/recovery/main_test.go | 2 +- examples/go/selfservice/settings/main_test.go | 2 +- .../go/selfservice/verification/main_test.go | 2 +- internal/httpclient/.gitignore | 24 - internal/httpclient/.openapi-generator-ignore | 23 - internal/httpclient/.openapi-generator/FILES | 260 - .../httpclient/.openapi-generator/VERSION | 1 - internal/httpclient/.travis.yml | 8 - internal/httpclient/README.md | 291 - internal/httpclient/api_courier.go | 362 - internal/httpclient/api_frontend.go | 5863 ----------------- internal/httpclient/api_metadata.go | 442 -- internal/httpclient/client.go | 550 -- internal/httpclient/configuration.go | 230 - internal/httpclient/git_push.sh | 58 - .../model_authenticator_assurance_level.go | 86 - .../model_batch_patch_identities_response.go | 115 - .../model_consistency_request_parameters.go | 115 - internal/httpclient/model_continue_with.go | 284 - .../model_continue_with_recovery_ui.go | 137 - .../model_continue_with_recovery_ui_flow.go | 145 - ...model_continue_with_redirect_browser_to.go | 138 - ...del_continue_with_set_ory_session_token.go | 138 - .../model_continue_with_settings_ui.go | 137 - .../model_continue_with_settings_ui_flow.go | 145 - .../model_continue_with_verification_ui.go | 137 - ...odel_continue_with_verification_ui_flow.go | 175 - .../model_courier_message_status.go | 86 - .../httpclient/model_courier_message_type.go | 84 - .../httpclient/model_create_identity_body.go | 361 - ..._create_recovery_link_for_identity_body.go | 145 - .../model_delete_my_sessions_count.go | 115 - ...enticator_assurance_level_not_satisfied.go | 151 - ..._error_browser_location_change_required.go | 151 - .../httpclient/model_error_flow_replaced.go | 151 - internal/httpclient/model_error_generic.go | 107 - internal/httpclient/model_flow_error.go | 219 - internal/httpclient/model_generic_error.go | 367 -- .../model_get_version_200_response.go | 108 - .../model_health_not_ready_status.go | 115 - internal/httpclient/model_health_status.go | 115 - internal/httpclient/model_identity.go | 582 -- .../httpclient/model_identity_credentials.go | 300 - .../model_identity_credentials_code.go | 163 - .../model_identity_credentials_oidc.go | 114 - ...odel_identity_credentials_oidc_provider.go | 294 - internal/httpclient/model_identity_patch.go | 151 - .../model_identity_patch_response.go | 189 - .../model_identity_schema_container.go | 152 - .../model_identity_with_credentials.go | 150 - .../model_identity_with_credentials_oidc.go | 114 - ...l_identity_with_credentials_oidc_config.go | 151 - ...y_with_credentials_oidc_config_provider.go | 138 - ...odel_identity_with_credentials_password.go | 114 - ...entity_with_credentials_password_config.go | 152 - .../httpclient/model_is_alive_200_response.go | 108 - .../httpclient/model_is_ready_503_response.go | 108 - internal/httpclient/model_json_patch.go | 213 - internal/httpclient/model_login_flow.go | 705 -- internal/httpclient/model_login_flow_state.go | 85 - internal/httpclient/model_logout_flow.go | 138 - internal/httpclient/model_message.go | 445 -- internal/httpclient/model_message_dispatch.go | 265 - .../model_needs_privileged_session_error.go | 144 - internal/httpclient/model_o_auth2_client.go | 1848 ------ ...consent_request_open_id_connect_context.go | 263 - .../httpclient/model_o_auth2_login_request.go | 407 -- .../httpclient/model_patch_identities_body.go | 115 - .../model_perform_native_logout_body.go | 108 - .../model_recovery_code_for_identity.go | 176 - internal/httpclient/model_recovery_flow.go | 438 -- .../httpclient/model_recovery_flow_state.go | 85 - .../model_recovery_identity_address.go | 240 - .../model_recovery_link_for_identity.go | 146 - .../httpclient/model_registration_flow.go | 558 -- .../model_registration_flow_state.go | 85 - .../model_self_service_flow_expired_error.go | 226 - internal/httpclient/model_session.go | 440 -- .../model_session_authentication_method.go | 262 - internal/httpclient/model_session_device.go | 219 - internal/httpclient/model_settings_flow.go | 467 -- .../httpclient/model_settings_flow_state.go | 84 - ...model_successful_code_exchange_response.go | 144 - .../model_successful_native_login.go | 181 - .../model_successful_native_registration.go | 217 - internal/httpclient/model_token_pagination.go | 160 - .../model_token_pagination_headers.go | 152 - internal/httpclient/model_ui_container.go | 203 - internal/httpclient/model_ui_node.go | 225 - .../model_ui_node_anchor_attributes.go | 197 - .../httpclient/model_ui_node_attributes.go | 284 - .../model_ui_node_image_attributes.go | 228 - .../model_ui_node_input_attributes.go | 568 -- internal/httpclient/model_ui_node_meta.go | 114 - .../model_ui_node_script_attributes.go | 348 - .../model_ui_node_text_attributes.go | 167 - internal/httpclient/model_ui_text.go | 204 - .../httpclient/model_update_identity_body.go | 280 - .../model_update_login_flow_body.go | 364 - ...odel_update_login_flow_with_code_method.go | 286 - ...te_login_flow_with_lookup_secret_method.go | 175 - ...odel_update_login_flow_with_oidc_method.go | 360 - ...l_update_login_flow_with_passkey_method.go | 182 - ..._update_login_flow_with_password_method.go | 279 - ...odel_update_login_flow_with_totp_method.go | 212 - ...update_login_flow_with_web_authn_method.go | 249 - .../model_update_recovery_flow_body.go | 164 - ...l_update_recovery_flow_with_code_method.go | 256 - ...l_update_recovery_flow_with_link_method.go | 212 - .../model_update_registration_flow_body.go | 324 - ...date_registration_flow_with_code_method.go | 286 - ...date_registration_flow_with_oidc_method.go | 360 - ...e_registration_flow_with_passkey_method.go | 249 - ..._registration_flow_with_password_method.go | 242 - ...e_registration_flow_with_profile_method.go | 249 - ...registration_flow_with_web_authn_method.go | 286 - .../model_update_settings_flow_body.go | 364 - ...update_settings_flow_with_lookup_method.go | 330 - ...l_update_settings_flow_with_oidc_method.go | 330 - ...pdate_settings_flow_with_passkey_method.go | 219 - ...date_settings_flow_with_password_method.go | 212 - ...pdate_settings_flow_with_profile_method.go | 212 - ...l_update_settings_flow_with_totp_method.go | 256 - ...ate_settings_flow_with_web_authn_method.go | 293 - .../model_update_verification_flow_body.go | 164 - ...date_verification_flow_with_code_method.go | 256 - ...date_verification_flow_with_link_method.go | 212 - .../model_verifiable_identity_address.go | 346 - .../httpclient/model_verification_flow.go | 422 -- .../model_verification_flow_state.go | 85 - internal/httpclient/model_version.go | 115 - internal/httpclient/response.go | 48 - internal/httpclient/utils.go | 329 - internal/registrationhelpers/helpers.go | 2 +- internal/testhelpers/sdk.go | 2 +- internal/testhelpers/selfservice_login.go | 2 +- internal/testhelpers/selfservice_recovery.go | 2 +- .../testhelpers/selfservice_registration.go | 2 +- internal/testhelpers/selfservice_settings.go | 2 +- .../testhelpers/selfservice_verification.go | 2 +- selfservice/flow/settings/handler_test.go | 2 +- .../strategy/code/strategy_login_test.go | 2 +- .../code/strategy_recovery_admin_test.go | 2 +- .../strategy/code/strategy_recovery_test.go | 2 +- .../code/strategy_registration_test.go | 2 +- .../strategy/link/strategy_recovery_test.go | 12 + selfservice/strategy/lookup/settings_test.go | 2 +- .../strategy/oidc/strategy_settings_test.go | 2 +- .../strategy/passkey/testfixture_test.go | 2 +- selfservice/strategy/password/login_test.go | 15 +- .../strategy/password/settings_test.go | 2 +- selfservice/strategy/profile/strategy_test.go | 2 +- selfservice/strategy/totp/settings_test.go | 2 +- selfservice/strategy/webauthn/login_test.go | 2 +- .../strategy/webauthn/registration_test.go | 2 +- .../strategy/webauthn/settings_test.go | 2 +- spec/api.json | 74 +- spec/swagger.json | 73 +- 163 files changed, 147 insertions(+), 35966 deletions(-) delete mode 100644 internal/httpclient/.gitignore delete mode 100644 internal/httpclient/.openapi-generator-ignore delete mode 100644 internal/httpclient/.openapi-generator/FILES delete mode 100644 internal/httpclient/.openapi-generator/VERSION delete mode 100644 internal/httpclient/.travis.yml delete mode 100644 internal/httpclient/README.md delete mode 100644 internal/httpclient/api_courier.go delete mode 100644 internal/httpclient/api_frontend.go delete mode 100644 internal/httpclient/api_metadata.go delete mode 100644 internal/httpclient/client.go delete mode 100644 internal/httpclient/configuration.go delete mode 100644 internal/httpclient/git_push.sh delete mode 100644 internal/httpclient/model_authenticator_assurance_level.go delete mode 100644 internal/httpclient/model_batch_patch_identities_response.go delete mode 100644 internal/httpclient/model_consistency_request_parameters.go delete mode 100644 internal/httpclient/model_continue_with.go delete mode 100644 internal/httpclient/model_continue_with_recovery_ui.go delete mode 100644 internal/httpclient/model_continue_with_recovery_ui_flow.go delete mode 100644 internal/httpclient/model_continue_with_redirect_browser_to.go delete mode 100644 internal/httpclient/model_continue_with_set_ory_session_token.go delete mode 100644 internal/httpclient/model_continue_with_settings_ui.go delete mode 100644 internal/httpclient/model_continue_with_settings_ui_flow.go delete mode 100644 internal/httpclient/model_continue_with_verification_ui.go delete mode 100644 internal/httpclient/model_continue_with_verification_ui_flow.go delete mode 100644 internal/httpclient/model_courier_message_status.go delete mode 100644 internal/httpclient/model_courier_message_type.go delete mode 100644 internal/httpclient/model_create_identity_body.go delete mode 100644 internal/httpclient/model_create_recovery_link_for_identity_body.go delete mode 100644 internal/httpclient/model_delete_my_sessions_count.go delete mode 100644 internal/httpclient/model_error_authenticator_assurance_level_not_satisfied.go delete mode 100644 internal/httpclient/model_error_browser_location_change_required.go delete mode 100644 internal/httpclient/model_error_flow_replaced.go delete mode 100644 internal/httpclient/model_error_generic.go delete mode 100644 internal/httpclient/model_flow_error.go delete mode 100644 internal/httpclient/model_generic_error.go delete mode 100644 internal/httpclient/model_get_version_200_response.go delete mode 100644 internal/httpclient/model_health_not_ready_status.go delete mode 100644 internal/httpclient/model_health_status.go delete mode 100644 internal/httpclient/model_identity.go delete mode 100644 internal/httpclient/model_identity_credentials.go delete mode 100644 internal/httpclient/model_identity_credentials_code.go delete mode 100644 internal/httpclient/model_identity_credentials_oidc.go delete mode 100644 internal/httpclient/model_identity_credentials_oidc_provider.go delete mode 100644 internal/httpclient/model_identity_patch.go delete mode 100644 internal/httpclient/model_identity_patch_response.go delete mode 100644 internal/httpclient/model_identity_schema_container.go delete mode 100644 internal/httpclient/model_identity_with_credentials.go delete mode 100644 internal/httpclient/model_identity_with_credentials_oidc.go delete mode 100644 internal/httpclient/model_identity_with_credentials_oidc_config.go delete mode 100644 internal/httpclient/model_identity_with_credentials_oidc_config_provider.go delete mode 100644 internal/httpclient/model_identity_with_credentials_password.go delete mode 100644 internal/httpclient/model_identity_with_credentials_password_config.go delete mode 100644 internal/httpclient/model_is_alive_200_response.go delete mode 100644 internal/httpclient/model_is_ready_503_response.go delete mode 100644 internal/httpclient/model_json_patch.go delete mode 100644 internal/httpclient/model_login_flow.go delete mode 100644 internal/httpclient/model_login_flow_state.go delete mode 100644 internal/httpclient/model_logout_flow.go delete mode 100644 internal/httpclient/model_message.go delete mode 100644 internal/httpclient/model_message_dispatch.go delete mode 100644 internal/httpclient/model_needs_privileged_session_error.go delete mode 100644 internal/httpclient/model_o_auth2_client.go delete mode 100644 internal/httpclient/model_o_auth2_consent_request_open_id_connect_context.go delete mode 100644 internal/httpclient/model_o_auth2_login_request.go delete mode 100644 internal/httpclient/model_patch_identities_body.go delete mode 100644 internal/httpclient/model_perform_native_logout_body.go delete mode 100644 internal/httpclient/model_recovery_code_for_identity.go delete mode 100644 internal/httpclient/model_recovery_flow.go delete mode 100644 internal/httpclient/model_recovery_flow_state.go delete mode 100644 internal/httpclient/model_recovery_identity_address.go delete mode 100644 internal/httpclient/model_recovery_link_for_identity.go delete mode 100644 internal/httpclient/model_registration_flow.go delete mode 100644 internal/httpclient/model_registration_flow_state.go delete mode 100644 internal/httpclient/model_self_service_flow_expired_error.go delete mode 100644 internal/httpclient/model_session.go delete mode 100644 internal/httpclient/model_session_authentication_method.go delete mode 100644 internal/httpclient/model_session_device.go delete mode 100644 internal/httpclient/model_settings_flow.go delete mode 100644 internal/httpclient/model_settings_flow_state.go delete mode 100644 internal/httpclient/model_successful_code_exchange_response.go delete mode 100644 internal/httpclient/model_successful_native_login.go delete mode 100644 internal/httpclient/model_successful_native_registration.go delete mode 100644 internal/httpclient/model_token_pagination.go delete mode 100644 internal/httpclient/model_token_pagination_headers.go delete mode 100644 internal/httpclient/model_ui_container.go delete mode 100644 internal/httpclient/model_ui_node.go delete mode 100644 internal/httpclient/model_ui_node_anchor_attributes.go delete mode 100644 internal/httpclient/model_ui_node_attributes.go delete mode 100644 internal/httpclient/model_ui_node_image_attributes.go delete mode 100644 internal/httpclient/model_ui_node_input_attributes.go delete mode 100644 internal/httpclient/model_ui_node_meta.go delete mode 100644 internal/httpclient/model_ui_node_script_attributes.go delete mode 100644 internal/httpclient/model_ui_node_text_attributes.go delete mode 100644 internal/httpclient/model_ui_text.go delete mode 100644 internal/httpclient/model_update_identity_body.go delete mode 100644 internal/httpclient/model_update_login_flow_body.go delete mode 100644 internal/httpclient/model_update_login_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_lookup_secret_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_oidc_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_passkey_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_password_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_totp_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_web_authn_method.go delete mode 100644 internal/httpclient/model_update_recovery_flow_body.go delete mode 100644 internal/httpclient/model_update_recovery_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_recovery_flow_with_link_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_body.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_oidc_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_passkey_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_password_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_profile_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_web_authn_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_body.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_lookup_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_oidc_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_passkey_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_password_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_profile_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_totp_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_web_authn_method.go delete mode 100644 internal/httpclient/model_update_verification_flow_body.go delete mode 100644 internal/httpclient/model_update_verification_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_verification_flow_with_link_method.go delete mode 100644 internal/httpclient/model_verifiable_identity_address.go delete mode 100644 internal/httpclient/model_verification_flow.go delete mode 100644 internal/httpclient/model_verification_flow_state.go delete mode 100644 internal/httpclient/model_version.go delete mode 100644 internal/httpclient/response.go delete mode 100644 internal/httpclient/utils.go diff --git a/cmd/cliclient/client.go b/cmd/cliclient/client.go index 82a41f2aacb1..fc7a4bed451c 100644 --- a/cmd/cliclient/client.go +++ b/cmd/cliclient/client.go @@ -18,7 +18,7 @@ import ( "github.com/spf13/pflag" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" ) const ( diff --git a/cmd/identities/definitions.go b/cmd/identities/definitions.go index 956266e70aa3..876eeba9f4a9 100644 --- a/cmd/identities/definitions.go +++ b/cmd/identities/definitions.go @@ -6,7 +6,7 @@ package identities import ( "strings" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" "github.com/ory/x/cmdx" ) diff --git a/cmd/identities/get.go b/cmd/identities/get.go index 677ac3bc9121..a575af2920e9 100644 --- a/cmd/identities/get.go +++ b/cmd/identities/get.go @@ -6,7 +6,7 @@ package identities import ( "fmt" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" "github.com/ory/kratos/x" "github.com/ory/x/cmdx" "github.com/ory/x/stringsx" diff --git a/cmd/identities/import.go b/cmd/identities/import.go index 1de8a22de385..16641f919477 100644 --- a/cmd/identities/import.go +++ b/cmd/identities/import.go @@ -7,7 +7,7 @@ import ( "encoding/json" "fmt" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" "github.com/ory/x/cmdx" diff --git a/cmd/identities/import_test.go b/cmd/identities/import_test.go index 8db159de9cff..1e8031b906ac 100644 --- a/cmd/identities/import_test.go +++ b/cmd/identities/import_test.go @@ -19,8 +19,8 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + kratos "github.com/ory/client-go" "github.com/ory/kratos/driver/config" - kratos "github.com/ory/kratos/internal/httpclient" ) func TestImportCmd(t *testing.T) { diff --git a/examples/go/selfservice/recovery/main_test.go b/examples/go/selfservice/recovery/main_test.go index b4ca43c511d6..d37a561227eb 100644 --- a/examples/go/selfservice/recovery/main_test.go +++ b/examples/go/selfservice/recovery/main_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ory "github.com/ory/client-go" "github.com/ory/kratos/examples/go/pkg" - ory "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" ) diff --git a/examples/go/selfservice/settings/main_test.go b/examples/go/selfservice/settings/main_test.go index 1dd5fa8cf77c..12518930724a 100644 --- a/examples/go/selfservice/settings/main_test.go +++ b/examples/go/selfservice/settings/main_test.go @@ -6,7 +6,7 @@ package main import ( "testing" - ory "github.com/ory/kratos/internal/httpclient" + ory "github.com/ory/client-go" "github.com/stretchr/testify/assert" diff --git a/examples/go/selfservice/verification/main_test.go b/examples/go/selfservice/verification/main_test.go index ca9ba687fb12..6a6621a15657 100644 --- a/examples/go/selfservice/verification/main_test.go +++ b/examples/go/selfservice/verification/main_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ory "github.com/ory/client-go" "github.com/ory/kratos/examples/go/pkg" - ory "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" ) diff --git a/internal/httpclient/.gitignore b/internal/httpclient/.gitignore deleted file mode 100644 index daf913b1b347..000000000000 --- a/internal/httpclient/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/internal/httpclient/.openapi-generator-ignore b/internal/httpclient/.openapi-generator-ignore deleted file mode 100644 index 7484ee590a38..000000000000 --- a/internal/httpclient/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES deleted file mode 100644 index 8f05b235508f..000000000000 --- a/internal/httpclient/.openapi-generator/FILES +++ /dev/null @@ -1,260 +0,0 @@ -.gitignore -.openapi-generator-ignore -.travis.yml -README.md -api/openapi.yaml -api_courier.go -api_frontend.go -api_identity.go -api_metadata.go -client.go -configuration.go -docs/AuthenticatorAssuranceLevel.md -docs/BatchPatchIdentitiesResponse.md -docs/ConsistencyRequestParameters.md -docs/ContinueWith.md -docs/ContinueWithRecoveryUi.md -docs/ContinueWithRecoveryUiFlow.md -docs/ContinueWithRedirectBrowserTo.md -docs/ContinueWithSetOrySessionToken.md -docs/ContinueWithSettingsUi.md -docs/ContinueWithSettingsUiFlow.md -docs/ContinueWithVerificationUi.md -docs/ContinueWithVerificationUiFlow.md -docs/CourierApi.md -docs/CourierMessageStatus.md -docs/CourierMessageType.md -docs/CreateIdentityBody.md -docs/CreateRecoveryCodeForIdentityBody.md -docs/CreateRecoveryLinkForIdentityBody.md -docs/DeleteMySessionsCount.md -docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md -docs/ErrorBrowserLocationChangeRequired.md -docs/ErrorFlowReplaced.md -docs/ErrorGeneric.md -docs/FlowError.md -docs/FrontendApi.md -docs/GenericError.md -docs/GetVersion200Response.md -docs/HealthNotReadyStatus.md -docs/HealthStatus.md -docs/Identity.md -docs/IdentityApi.md -docs/IdentityCredentials.md -docs/IdentityCredentialsCode.md -docs/IdentityCredentialsOidc.md -docs/IdentityCredentialsOidcProvider.md -docs/IdentityCredentialsPassword.md -docs/IdentityPatch.md -docs/IdentityPatchResponse.md -docs/IdentitySchemaContainer.md -docs/IdentityWithCredentials.md -docs/IdentityWithCredentialsOidc.md -docs/IdentityWithCredentialsOidcConfig.md -docs/IdentityWithCredentialsOidcConfigProvider.md -docs/IdentityWithCredentialsPassword.md -docs/IdentityWithCredentialsPasswordConfig.md -docs/IsAlive200Response.md -docs/IsReady503Response.md -docs/JsonPatch.md -docs/LoginFlow.md -docs/LoginFlowState.md -docs/LogoutFlow.md -docs/Message.md -docs/MessageDispatch.md -docs/MetadataApi.md -docs/NeedsPrivilegedSessionError.md -docs/OAuth2Client.md -docs/OAuth2ConsentRequestOpenIDConnectContext.md -docs/OAuth2LoginRequest.md -docs/PatchIdentitiesBody.md -docs/PerformNativeLogoutBody.md -docs/RecoveryCodeForIdentity.md -docs/RecoveryFlow.md -docs/RecoveryFlowState.md -docs/RecoveryIdentityAddress.md -docs/RecoveryLinkForIdentity.md -docs/RegistrationFlow.md -docs/RegistrationFlowState.md -docs/SelfServiceFlowExpiredError.md -docs/Session.md -docs/SessionAuthenticationMethod.md -docs/SessionDevice.md -docs/SettingsFlow.md -docs/SettingsFlowState.md -docs/SuccessfulCodeExchangeResponse.md -docs/SuccessfulNativeLogin.md -docs/SuccessfulNativeRegistration.md -docs/TokenPagination.md -docs/TokenPaginationHeaders.md -docs/UiContainer.md -docs/UiNode.md -docs/UiNodeAnchorAttributes.md -docs/UiNodeAttributes.md -docs/UiNodeImageAttributes.md -docs/UiNodeInputAttributes.md -docs/UiNodeMeta.md -docs/UiNodeScriptAttributes.md -docs/UiNodeTextAttributes.md -docs/UiText.md -docs/UpdateIdentityBody.md -docs/UpdateLoginFlowBody.md -docs/UpdateLoginFlowWithCodeMethod.md -docs/UpdateLoginFlowWithLookupSecretMethod.md -docs/UpdateLoginFlowWithOidcMethod.md -docs/UpdateLoginFlowWithPasskeyMethod.md -docs/UpdateLoginFlowWithPasswordMethod.md -docs/UpdateLoginFlowWithTotpMethod.md -docs/UpdateLoginFlowWithWebAuthnMethod.md -docs/UpdateRecoveryFlowBody.md -docs/UpdateRecoveryFlowWithCodeMethod.md -docs/UpdateRecoveryFlowWithLinkMethod.md -docs/UpdateRegistrationFlowBody.md -docs/UpdateRegistrationFlowWithCodeMethod.md -docs/UpdateRegistrationFlowWithOidcMethod.md -docs/UpdateRegistrationFlowWithPasskeyMethod.md -docs/UpdateRegistrationFlowWithPasswordMethod.md -docs/UpdateRegistrationFlowWithProfileMethod.md -docs/UpdateRegistrationFlowWithWebAuthnMethod.md -docs/UpdateSettingsFlowBody.md -docs/UpdateSettingsFlowWithLookupMethod.md -docs/UpdateSettingsFlowWithOidcMethod.md -docs/UpdateSettingsFlowWithPasskeyMethod.md -docs/UpdateSettingsFlowWithPasswordMethod.md -docs/UpdateSettingsFlowWithProfileMethod.md -docs/UpdateSettingsFlowWithTotpMethod.md -docs/UpdateSettingsFlowWithWebAuthnMethod.md -docs/UpdateVerificationFlowBody.md -docs/UpdateVerificationFlowWithCodeMethod.md -docs/UpdateVerificationFlowWithLinkMethod.md -docs/VerifiableIdentityAddress.md -docs/VerificationFlow.md -docs/VerificationFlowState.md -docs/Version.md -git_push.sh -go.mod -go.sum -model_authenticator_assurance_level.go -model_batch_patch_identities_response.go -model_consistency_request_parameters.go -model_continue_with.go -model_continue_with_recovery_ui.go -model_continue_with_recovery_ui_flow.go -model_continue_with_redirect_browser_to.go -model_continue_with_set_ory_session_token.go -model_continue_with_settings_ui.go -model_continue_with_settings_ui_flow.go -model_continue_with_verification_ui.go -model_continue_with_verification_ui_flow.go -model_courier_message_status.go -model_courier_message_type.go -model_create_identity_body.go -model_create_recovery_code_for_identity_body.go -model_create_recovery_link_for_identity_body.go -model_delete_my_sessions_count.go -model_error_authenticator_assurance_level_not_satisfied.go -model_error_browser_location_change_required.go -model_error_flow_replaced.go -model_error_generic.go -model_flow_error.go -model_generic_error.go -model_get_version_200_response.go -model_health_not_ready_status.go -model_health_status.go -model_identity.go -model_identity_credentials.go -model_identity_credentials_code.go -model_identity_credentials_oidc.go -model_identity_credentials_oidc_provider.go -model_identity_credentials_password.go -model_identity_patch.go -model_identity_patch_response.go -model_identity_schema_container.go -model_identity_with_credentials.go -model_identity_with_credentials_oidc.go -model_identity_with_credentials_oidc_config.go -model_identity_with_credentials_oidc_config_provider.go -model_identity_with_credentials_password.go -model_identity_with_credentials_password_config.go -model_is_alive_200_response.go -model_is_ready_503_response.go -model_json_patch.go -model_login_flow.go -model_login_flow_state.go -model_logout_flow.go -model_message.go -model_message_dispatch.go -model_needs_privileged_session_error.go -model_o_auth2_client.go -model_o_auth2_consent_request_open_id_connect_context.go -model_o_auth2_login_request.go -model_patch_identities_body.go -model_perform_native_logout_body.go -model_recovery_code_for_identity.go -model_recovery_flow.go -model_recovery_flow_state.go -model_recovery_identity_address.go -model_recovery_link_for_identity.go -model_registration_flow.go -model_registration_flow_state.go -model_self_service_flow_expired_error.go -model_session.go -model_session_authentication_method.go -model_session_device.go -model_settings_flow.go -model_settings_flow_state.go -model_successful_code_exchange_response.go -model_successful_native_login.go -model_successful_native_registration.go -model_token_pagination.go -model_token_pagination_headers.go -model_ui_container.go -model_ui_node.go -model_ui_node_anchor_attributes.go -model_ui_node_attributes.go -model_ui_node_image_attributes.go -model_ui_node_input_attributes.go -model_ui_node_meta.go -model_ui_node_script_attributes.go -model_ui_node_text_attributes.go -model_ui_text.go -model_update_identity_body.go -model_update_login_flow_body.go -model_update_login_flow_with_code_method.go -model_update_login_flow_with_lookup_secret_method.go -model_update_login_flow_with_oidc_method.go -model_update_login_flow_with_passkey_method.go -model_update_login_flow_with_password_method.go -model_update_login_flow_with_totp_method.go -model_update_login_flow_with_web_authn_method.go -model_update_recovery_flow_body.go -model_update_recovery_flow_with_code_method.go -model_update_recovery_flow_with_link_method.go -model_update_registration_flow_body.go -model_update_registration_flow_with_code_method.go -model_update_registration_flow_with_oidc_method.go -model_update_registration_flow_with_passkey_method.go -model_update_registration_flow_with_password_method.go -model_update_registration_flow_with_profile_method.go -model_update_registration_flow_with_web_authn_method.go -model_update_settings_flow_body.go -model_update_settings_flow_with_lookup_method.go -model_update_settings_flow_with_oidc_method.go -model_update_settings_flow_with_passkey_method.go -model_update_settings_flow_with_password_method.go -model_update_settings_flow_with_profile_method.go -model_update_settings_flow_with_totp_method.go -model_update_settings_flow_with_web_authn_method.go -model_update_verification_flow_body.go -model_update_verification_flow_with_code_method.go -model_update_verification_flow_with_link_method.go -model_verifiable_identity_address.go -model_verification_flow.go -model_verification_flow_state.go -model_version.go -response.go -test/api_courier_test.go -test/api_frontend_test.go -test/api_identity_test.go -test/api_metadata_test.go -utils.go diff --git a/internal/httpclient/.openapi-generator/VERSION b/internal/httpclient/.openapi-generator/VERSION deleted file mode 100644 index 4b49d9bb63ee..000000000000 --- a/internal/httpclient/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.2.0 \ No newline at end of file diff --git a/internal/httpclient/.travis.yml b/internal/httpclient/.travis.yml deleted file mode 100644 index f5cb2ce9a5aa..000000000000 --- a/internal/httpclient/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -install: - - go get -d -v . - -script: - - go build -v ./ - diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md deleted file mode 100644 index 01f9831e7520..000000000000 --- a/internal/httpclient/README.md +++ /dev/null @@ -1,291 +0,0 @@ -# Go API client for client - -This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. - - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. - -- API version: -- Package version: 1.0.0 -- Build package: org.openapitools.codegen.languages.GoClientCodegen - -## Installation - -Install the following dependencies: - -```shell -go get github.com/stretchr/testify/assert -go get golang.org/x/oauth2 -go get golang.org/x/net/context -``` - -Put the package under your project folder and add the following in import: - -```golang -import client "github.com/ory/client-go" -``` - -To use a proxy, set the environment variable `HTTP_PROXY`: - -```golang -os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") -``` - -## Configuration of Server URL - -Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. - -### Select Server Configuration - -For using other server than the one defined on index 0 set context value `sw.ContextServerIndex` of type `int`. - -```golang -ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) -``` - -### Templated Server URL - -Templated server URL is formatted using default variables from configuration or from context value `sw.ContextServerVariables` of type `map[string]string`. - -```golang -ctx := context.WithValue(context.Background(), client.ContextServerVariables, map[string]string{ - "basePath": "v2", -}) -``` - -Note, enum values are always validated and all unused variables are silently ignored. - -### URLs Configuration per Operation - -Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. -An operation is uniquely identifield by `"{classname}Service.{nickname}"` string. -Similar rules for overriding default operation server index and variables applies by using `sw.ContextOperationServerIndices` and `sw.ContextOperationServerVariables` context maps. - -``` -ctx := context.WithValue(context.Background(), client.ContextOperationServerIndices, map[string]int{ - "{classname}Service.{nickname}": 2, -}) -ctx = context.WithValue(context.Background(), client.ContextOperationServerVariables, map[string]map[string]string{ - "{classname}Service.{nickname}": { - "port": "8443", - }, -}) -``` - -## Documentation for API Endpoints - -All URIs are relative to *http://localhost* - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*CourierApi* | [**GetCourierMessage**](docs/CourierApi.md#getcouriermessage) | **Get** /admin/courier/messages/{id} | Get a Message -*CourierApi* | [**ListCourierMessages**](docs/CourierApi.md#listcouriermessages) | **Get** /admin/courier/messages | List Messages -*FrontendApi* | [**CreateBrowserLoginFlow**](docs/FrontendApi.md#createbrowserloginflow) | **Get** /self-service/login/browser | Create Login Flow for Browsers -*FrontendApi* | [**CreateBrowserLogoutFlow**](docs/FrontendApi.md#createbrowserlogoutflow) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers -*FrontendApi* | [**CreateBrowserRecoveryFlow**](docs/FrontendApi.md#createbrowserrecoveryflow) | **Get** /self-service/recovery/browser | Create Recovery Flow for Browsers -*FrontendApi* | [**CreateBrowserRegistrationFlow**](docs/FrontendApi.md#createbrowserregistrationflow) | **Get** /self-service/registration/browser | Create Registration Flow for Browsers -*FrontendApi* | [**CreateBrowserSettingsFlow**](docs/FrontendApi.md#createbrowsersettingsflow) | **Get** /self-service/settings/browser | Create Settings Flow for Browsers -*FrontendApi* | [**CreateBrowserVerificationFlow**](docs/FrontendApi.md#createbrowserverificationflow) | **Get** /self-service/verification/browser | Create Verification Flow for Browser Clients -*FrontendApi* | [**CreateNativeLoginFlow**](docs/FrontendApi.md#createnativeloginflow) | **Get** /self-service/login/api | Create Login Flow for Native Apps -*FrontendApi* | [**CreateNativeRecoveryFlow**](docs/FrontendApi.md#createnativerecoveryflow) | **Get** /self-service/recovery/api | Create Recovery Flow for Native Apps -*FrontendApi* | [**CreateNativeRegistrationFlow**](docs/FrontendApi.md#createnativeregistrationflow) | **Get** /self-service/registration/api | Create Registration Flow for Native Apps -*FrontendApi* | [**CreateNativeSettingsFlow**](docs/FrontendApi.md#createnativesettingsflow) | **Get** /self-service/settings/api | Create Settings Flow for Native Apps -*FrontendApi* | [**CreateNativeVerificationFlow**](docs/FrontendApi.md#createnativeverificationflow) | **Get** /self-service/verification/api | Create Verification Flow for Native Apps -*FrontendApi* | [**DisableMyOtherSessions**](docs/FrontendApi.md#disablemyothersessions) | **Delete** /sessions | Disable my other sessions -*FrontendApi* | [**DisableMySession**](docs/FrontendApi.md#disablemysession) | **Delete** /sessions/{id} | Disable one of my sessions -*FrontendApi* | [**ExchangeSessionToken**](docs/FrontendApi.md#exchangesessiontoken) | **Get** /sessions/token-exchange | Exchange Session Token -*FrontendApi* | [**GetFlowError**](docs/FrontendApi.md#getflowerror) | **Get** /self-service/errors | Get User-Flow Errors -*FrontendApi* | [**GetLoginFlow**](docs/FrontendApi.md#getloginflow) | **Get** /self-service/login/flows | Get Login Flow -*FrontendApi* | [**GetRecoveryFlow**](docs/FrontendApi.md#getrecoveryflow) | **Get** /self-service/recovery/flows | Get Recovery Flow -*FrontendApi* | [**GetRegistrationFlow**](docs/FrontendApi.md#getregistrationflow) | **Get** /self-service/registration/flows | Get Registration Flow -*FrontendApi* | [**GetSettingsFlow**](docs/FrontendApi.md#getsettingsflow) | **Get** /self-service/settings/flows | Get Settings Flow -*FrontendApi* | [**GetVerificationFlow**](docs/FrontendApi.md#getverificationflow) | **Get** /self-service/verification/flows | Get Verification Flow -*FrontendApi* | [**GetWebAuthnJavaScript**](docs/FrontendApi.md#getwebauthnjavascript) | **Get** /.well-known/ory/webauthn.js | Get WebAuthn JavaScript -*FrontendApi* | [**ListMySessions**](docs/FrontendApi.md#listmysessions) | **Get** /sessions | Get My Active Sessions -*FrontendApi* | [**PerformNativeLogout**](docs/FrontendApi.md#performnativelogout) | **Delete** /self-service/logout/api | Perform Logout for Native Apps -*FrontendApi* | [**ToSession**](docs/FrontendApi.md#tosession) | **Get** /sessions/whoami | Check Who the Current HTTP Session Belongs To -*FrontendApi* | [**UpdateLoginFlow**](docs/FrontendApi.md#updateloginflow) | **Post** /self-service/login | Submit a Login Flow -*FrontendApi* | [**UpdateLogoutFlow**](docs/FrontendApi.md#updatelogoutflow) | **Get** /self-service/logout | Update Logout Flow -*FrontendApi* | [**UpdateRecoveryFlow**](docs/FrontendApi.md#updaterecoveryflow) | **Post** /self-service/recovery | Update Recovery Flow -*FrontendApi* | [**UpdateRegistrationFlow**](docs/FrontendApi.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow -*FrontendApi* | [**UpdateSettingsFlow**](docs/FrontendApi.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow -*FrontendApi* | [**UpdateVerificationFlow**](docs/FrontendApi.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow -*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create multiple identities -*IdentityApi* | [**CreateIdentity**](docs/IdentityApi.md#createidentity) | **Post** /admin/identities | Create an Identity -*IdentityApi* | [**CreateRecoveryCodeForIdentity**](docs/IdentityApi.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code -*IdentityApi* | [**CreateRecoveryLinkForIdentity**](docs/IdentityApi.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link -*IdentityApi* | [**DeleteIdentity**](docs/IdentityApi.md#deleteidentity) | **Delete** /admin/identities/{id} | Delete an Identity -*IdentityApi* | [**DeleteIdentityCredentials**](docs/IdentityApi.md#deleteidentitycredentials) | **Delete** /admin/identities/{id}/credentials/{type} | Delete a credential for a specific identity -*IdentityApi* | [**DeleteIdentitySessions**](docs/IdentityApi.md#deleteidentitysessions) | **Delete** /admin/identities/{id}/sessions | Delete & Invalidate an Identity's Sessions -*IdentityApi* | [**DisableSession**](docs/IdentityApi.md#disablesession) | **Delete** /admin/sessions/{id} | Deactivate a Session -*IdentityApi* | [**ExtendSession**](docs/IdentityApi.md#extendsession) | **Patch** /admin/sessions/{id}/extend | Extend a Session -*IdentityApi* | [**GetIdentity**](docs/IdentityApi.md#getidentity) | **Get** /admin/identities/{id} | Get an Identity -*IdentityApi* | [**GetIdentitySchema**](docs/IdentityApi.md#getidentityschema) | **Get** /schemas/{id} | Get Identity JSON Schema -*IdentityApi* | [**GetSession**](docs/IdentityApi.md#getsession) | **Get** /admin/sessions/{id} | Get Session -*IdentityApi* | [**ListIdentities**](docs/IdentityApi.md#listidentities) | **Get** /admin/identities | List Identities -*IdentityApi* | [**ListIdentitySchemas**](docs/IdentityApi.md#listidentityschemas) | **Get** /schemas | Get all Identity Schemas -*IdentityApi* | [**ListIdentitySessions**](docs/IdentityApi.md#listidentitysessions) | **Get** /admin/identities/{id}/sessions | List an Identity's Sessions -*IdentityApi* | [**ListSessions**](docs/IdentityApi.md#listsessions) | **Get** /admin/sessions | List All Sessions -*IdentityApi* | [**PatchIdentity**](docs/IdentityApi.md#patchidentity) | **Patch** /admin/identities/{id} | Patch an Identity -*IdentityApi* | [**UpdateIdentity**](docs/IdentityApi.md#updateidentity) | **Put** /admin/identities/{id} | Update an Identity -*MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. -*MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status -*MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status - - -## Documentation For Models - - - [AuthenticatorAssuranceLevel](docs/AuthenticatorAssuranceLevel.md) - - [BatchPatchIdentitiesResponse](docs/BatchPatchIdentitiesResponse.md) - - [ConsistencyRequestParameters](docs/ConsistencyRequestParameters.md) - - [ContinueWith](docs/ContinueWith.md) - - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) - - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) - - [ContinueWithVerificationUi](docs/ContinueWithVerificationUi.md) - - [ContinueWithVerificationUiFlow](docs/ContinueWithVerificationUiFlow.md) - - [CourierMessageStatus](docs/CourierMessageStatus.md) - - [CourierMessageType](docs/CourierMessageType.md) - - [CreateIdentityBody](docs/CreateIdentityBody.md) - - [CreateRecoveryCodeForIdentityBody](docs/CreateRecoveryCodeForIdentityBody.md) - - [CreateRecoveryLinkForIdentityBody](docs/CreateRecoveryLinkForIdentityBody.md) - - [DeleteMySessionsCount](docs/DeleteMySessionsCount.md) - - [ErrorAuthenticatorAssuranceLevelNotSatisfied](docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md) - - [ErrorBrowserLocationChangeRequired](docs/ErrorBrowserLocationChangeRequired.md) - - [ErrorFlowReplaced](docs/ErrorFlowReplaced.md) - - [ErrorGeneric](docs/ErrorGeneric.md) - - [FlowError](docs/FlowError.md) - - [GenericError](docs/GenericError.md) - - [GetVersion200Response](docs/GetVersion200Response.md) - - [HealthNotReadyStatus](docs/HealthNotReadyStatus.md) - - [HealthStatus](docs/HealthStatus.md) - - [Identity](docs/Identity.md) - - [IdentityCredentials](docs/IdentityCredentials.md) - - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) - - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) - - [IdentityPatch](docs/IdentityPatch.md) - - [IdentityPatchResponse](docs/IdentityPatchResponse.md) - - [IdentitySchemaContainer](docs/IdentitySchemaContainer.md) - - [IdentityWithCredentials](docs/IdentityWithCredentials.md) - - [IdentityWithCredentialsOidc](docs/IdentityWithCredentialsOidc.md) - - [IdentityWithCredentialsOidcConfig](docs/IdentityWithCredentialsOidcConfig.md) - - [IdentityWithCredentialsOidcConfigProvider](docs/IdentityWithCredentialsOidcConfigProvider.md) - - [IdentityWithCredentialsPassword](docs/IdentityWithCredentialsPassword.md) - - [IdentityWithCredentialsPasswordConfig](docs/IdentityWithCredentialsPasswordConfig.md) - - [IsAlive200Response](docs/IsAlive200Response.md) - - [IsReady503Response](docs/IsReady503Response.md) - - [JsonPatch](docs/JsonPatch.md) - - [LoginFlow](docs/LoginFlow.md) - - [LoginFlowState](docs/LoginFlowState.md) - - [LogoutFlow](docs/LogoutFlow.md) - - [Message](docs/Message.md) - - [MessageDispatch](docs/MessageDispatch.md) - - [NeedsPrivilegedSessionError](docs/NeedsPrivilegedSessionError.md) - - [OAuth2Client](docs/OAuth2Client.md) - - [OAuth2ConsentRequestOpenIDConnectContext](docs/OAuth2ConsentRequestOpenIDConnectContext.md) - - [OAuth2LoginRequest](docs/OAuth2LoginRequest.md) - - [PatchIdentitiesBody](docs/PatchIdentitiesBody.md) - - [PerformNativeLogoutBody](docs/PerformNativeLogoutBody.md) - - [RecoveryCodeForIdentity](docs/RecoveryCodeForIdentity.md) - - [RecoveryFlow](docs/RecoveryFlow.md) - - [RecoveryFlowState](docs/RecoveryFlowState.md) - - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) - - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) - - [RegistrationFlow](docs/RegistrationFlow.md) - - [RegistrationFlowState](docs/RegistrationFlowState.md) - - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) - - [Session](docs/Session.md) - - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) - - [SessionDevice](docs/SessionDevice.md) - - [SettingsFlow](docs/SettingsFlow.md) - - [SettingsFlowState](docs/SettingsFlowState.md) - - [SuccessfulCodeExchangeResponse](docs/SuccessfulCodeExchangeResponse.md) - - [SuccessfulNativeLogin](docs/SuccessfulNativeLogin.md) - - [SuccessfulNativeRegistration](docs/SuccessfulNativeRegistration.md) - - [TokenPagination](docs/TokenPagination.md) - - [TokenPaginationHeaders](docs/TokenPaginationHeaders.md) - - [UiContainer](docs/UiContainer.md) - - [UiNode](docs/UiNode.md) - - [UiNodeAnchorAttributes](docs/UiNodeAnchorAttributes.md) - - [UiNodeAttributes](docs/UiNodeAttributes.md) - - [UiNodeImageAttributes](docs/UiNodeImageAttributes.md) - - [UiNodeInputAttributes](docs/UiNodeInputAttributes.md) - - [UiNodeMeta](docs/UiNodeMeta.md) - - [UiNodeScriptAttributes](docs/UiNodeScriptAttributes.md) - - [UiNodeTextAttributes](docs/UiNodeTextAttributes.md) - - [UiText](docs/UiText.md) - - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) - - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) - - [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md) - - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) - - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) - - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) - - [UpdateRecoveryFlowBody](docs/UpdateRecoveryFlowBody.md) - - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) - - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) - - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) - - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) - - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) - - [UpdateSettingsFlowWithOidcMethod](docs/UpdateSettingsFlowWithOidcMethod.md) - - [UpdateSettingsFlowWithPasskeyMethod](docs/UpdateSettingsFlowWithPasskeyMethod.md) - - [UpdateSettingsFlowWithPasswordMethod](docs/UpdateSettingsFlowWithPasswordMethod.md) - - [UpdateSettingsFlowWithProfileMethod](docs/UpdateSettingsFlowWithProfileMethod.md) - - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) - - [UpdateSettingsFlowWithWebAuthnMethod](docs/UpdateSettingsFlowWithWebAuthnMethod.md) - - [UpdateVerificationFlowBody](docs/UpdateVerificationFlowBody.md) - - [UpdateVerificationFlowWithCodeMethod](docs/UpdateVerificationFlowWithCodeMethod.md) - - [UpdateVerificationFlowWithLinkMethod](docs/UpdateVerificationFlowWithLinkMethod.md) - - [VerifiableIdentityAddress](docs/VerifiableIdentityAddress.md) - - [VerificationFlow](docs/VerificationFlow.md) - - [VerificationFlowState](docs/VerificationFlowState.md) - - [Version](docs/Version.md) - - -## Documentation For Authorization - - - -### oryAccessToken - -- **Type**: API key -- **API key parameter name**: Authorization -- **Location**: HTTP header - -Note, each API key must be added to a map of `map[string]APIKey` where the key is: Authorization and passed in as the auth context for each request. - - -## Documentation for Utility Methods - -Due to the fact that model structure members are all pointers, this package contains -a number of utility functions to easily obtain pointers to values of basic types. -Each of these functions takes a value of the given basic type and returns a pointer to it: - -* `PtrBool` -* `PtrInt` -* `PtrInt32` -* `PtrInt64` -* `PtrFloat` -* `PtrFloat32` -* `PtrFloat64` -* `PtrString` -* `PtrTime` - -## Author - -office@ory.sh - diff --git a/internal/httpclient/api_courier.go b/internal/httpclient/api_courier.go deleted file mode 100644 index 91bcc08025eb..000000000000 --- a/internal/httpclient/api_courier.go +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Ory Identities API - * - * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. - * - * API version: - * Contact: office@ory.sh - */ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package client - -import ( - "bytes" - "context" - "io" - "net/http" - "net/url" - "strings" -) - -// Linger please -var ( - _ context.Context -) - -type CourierApi interface { - - /* - * GetCourierMessage Get a Message - * Gets a specific messages by the given ID. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest - */ - GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest - - /* - * GetCourierMessageExecute executes the request - * @return Message - */ - GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) - - /* - * ListCourierMessages List Messages - * Lists all messages by given status and recipient. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest - */ - ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest - - /* - * ListCourierMessagesExecute executes the request - * @return []Message - */ - ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) -} - -// CourierApiService CourierApi service -type CourierApiService service - -type CourierApiApiGetCourierMessageRequest struct { - ctx context.Context - ApiService CourierApi - id string -} - -func (r CourierApiApiGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { - return r.ApiService.GetCourierMessageExecute(r) -} - -/* - * GetCourierMessage Get a Message - * Gets a specific messages by the given ID. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest - */ -func (a *CourierApiService) GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest { - return CourierApiApiGetCourierMessageRequest{ - ApiService: a, - ctx: ctx, - id: id, - } -} - -/* - * Execute executes the request - * @return Message - */ -func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - localVarFormFileName string - localVarFileName string - localVarFileBytes []byte - localVarReturnValue *Message - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.GetCourierMessage") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/admin/courier/messages/{id}" - localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterToString(r.id, "")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - if r.ctx != nil { - // API Key Authentication - if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { - if apiKey, ok := auth["oryAccessToken"]; ok { - var key string - if apiKey.Prefix != "" { - key = apiKey.Prefix + " " + apiKey.Key - } else { - key = apiKey.Key - } - localVarHeaderParams["Authorization"] = key - } - } - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - if localVarHTTPResponse.StatusCode == 400 { - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type CourierApiApiListCourierMessagesRequest struct { - ctx context.Context - ApiService CourierApi - pageSize *int64 - pageToken *string - status *CourierMessageStatus - recipient *string -} - -func (r CourierApiApiListCourierMessagesRequest) PageSize(pageSize int64) CourierApiApiListCourierMessagesRequest { - r.pageSize = &pageSize - return r -} -func (r CourierApiApiListCourierMessagesRequest) PageToken(pageToken string) CourierApiApiListCourierMessagesRequest { - r.pageToken = &pageToken - return r -} -func (r CourierApiApiListCourierMessagesRequest) Status(status CourierMessageStatus) CourierApiApiListCourierMessagesRequest { - r.status = &status - return r -} -func (r CourierApiApiListCourierMessagesRequest) Recipient(recipient string) CourierApiApiListCourierMessagesRequest { - r.recipient = &recipient - return r -} - -func (r CourierApiApiListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { - return r.ApiService.ListCourierMessagesExecute(r) -} - -/* - * ListCourierMessages List Messages - * Lists all messages by given status and recipient. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest - */ -func (a *CourierApiService) ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest { - return CourierApiApiListCourierMessagesRequest{ - ApiService: a, - ctx: ctx, - } -} - -/* - * Execute executes the request - * @return []Message - */ -func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - localVarFormFileName string - localVarFileName string - localVarFileBytes []byte - localVarReturnValue []Message - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.ListCourierMessages") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/admin/courier/messages" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - if r.pageSize != nil { - localVarQueryParams.Add("page_size", parameterToString(*r.pageSize, "")) - } - if r.pageToken != nil { - localVarQueryParams.Add("page_token", parameterToString(*r.pageToken, "")) - } - if r.status != nil { - localVarQueryParams.Add("status", parameterToString(*r.status, "")) - } - if r.recipient != nil { - localVarQueryParams.Add("recipient", parameterToString(*r.recipient, "")) - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - if r.ctx != nil { - // API Key Authentication - if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { - if apiKey, ok := auth["oryAccessToken"]; ok { - var key string - if apiKey.Prefix != "" { - key = apiKey.Prefix + " " + apiKey.Key - } else { - key = apiKey.Key - } - localVarHeaderParams["Authorization"] = key - } - } - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - if localVarHTTPResponse.StatusCode == 400 { - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} diff --git a/internal/httpclient/api_frontend.go b/internal/httpclient/api_frontend.go deleted file mode 100644 index cfb87b55902a..000000000000 --- a/internal/httpclient/api_frontend.go +++ /dev/null @@ -1,5863 +0,0 @@ -/* - * Ory Identities API - * - * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. - * - * API version: - * Contact: office@ory.sh - */ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package client - -import ( - "bytes" - "context" - "io" - "net/http" - "net/url" - "strings" -) - -// Linger please -var ( - _ context.Context -) - -type FrontendApi interface { - - /* - * CreateBrowserLoginFlow Create Login Flow for Browsers - * This endpoint initializes a browser-based user login flow. This endpoint will set the appropriate - cookies and anti-CSRF measures required for browser-based flows. - - If this endpoint is opened as a link in the browser, it will be redirected to - `selfservice.flows.login.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session - exists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter - `?refresh=true` was set. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `session_aal1_required`: Multi-factor auth (e.g. 2fa) was requested but the user has no session yet. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! - - The optional query parameter login_challenge is set when using Kratos with - Hydra in an OAuth2 flow. See the oauth2_provider.url configuration - option. - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLoginFlowRequest - */ - CreateBrowserLoginFlow(ctx context.Context) FrontendApiApiCreateBrowserLoginFlowRequest - - /* - * CreateBrowserLoginFlowExecute executes the request - * @return LoginFlow - */ - CreateBrowserLoginFlowExecute(r FrontendApiApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) - - /* - * CreateBrowserLogoutFlow Create a Logout URL for Browsers - * This endpoint initializes a browser-based user logout flow and a URL which can be used to log out the user. - - This endpoint is NOT INTENDED for API clients and only works - with browsers (Chrome, Firefox, ...). For API clients you can - call the `/self-service/logout/api` URL directly with the Ory Session Token. - - The URL is only valid for the currently signed in user. If no user is signed in, this endpoint returns - a 401 error. - - When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLogoutFlowRequest - */ - CreateBrowserLogoutFlow(ctx context.Context) FrontendApiApiCreateBrowserLogoutFlowRequest - - /* - * CreateBrowserLogoutFlowExecute executes the request - * @return LogoutFlow - */ - CreateBrowserLogoutFlowExecute(r FrontendApiApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) - - /* - * CreateBrowserRecoveryFlow Create Recovery Flow for Browsers - * This endpoint initializes a browser-based account recovery flow. Once initialized, the browser will be redirected to - `selfservice.flows.recovery.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session - exists, the browser is returned to the configured return URL. - - If this endpoint is called via an AJAX request, the response contains the recovery flow without any redirects - or a 400 bad request error if the user is already authenticated. - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRecoveryFlowRequest - */ - CreateBrowserRecoveryFlow(ctx context.Context) FrontendApiApiCreateBrowserRecoveryFlowRequest - - /* - * CreateBrowserRecoveryFlowExecute executes the request - * @return RecoveryFlow - */ - CreateBrowserRecoveryFlowExecute(r FrontendApiApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) - - /* - * CreateBrowserRegistrationFlow Create Registration Flow for Browsers - * This endpoint initializes a browser-based user registration flow. This endpoint will set the appropriate - cookies and anti-CSRF measures required for browser-based flows. - - If this endpoint is opened as a link in the browser, it will be redirected to - `selfservice.flows.registration.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session - exists already, the browser will be redirected to `urls.default_redirect_url`. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! - - If this endpoint is called via an AJAX request, the response contains the registration flow without a redirect. - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRegistrationFlowRequest - */ - CreateBrowserRegistrationFlow(ctx context.Context) FrontendApiApiCreateBrowserRegistrationFlowRequest - - /* - * CreateBrowserRegistrationFlowExecute executes the request - * @return RegistrationFlow - */ - CreateBrowserRegistrationFlowExecute(r FrontendApiApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) - - /* - * CreateBrowserSettingsFlow Create Settings Flow for Browsers - * This endpoint initializes a browser-based user settings flow. Once initialized, the browser will be redirected to - `selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid - Ory Kratos Session Cookie is included in the request, a login flow will be initialized. - - If this endpoint is opened as a link in the browser, it will be redirected to - `selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid user session - was set, the browser will be redirected to the login endpoint. - - If this endpoint is called via an AJAX request, the response contains the settings flow without any redirects - or a 401 forbidden error if no valid session was set. - - Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator - Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn - credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user - to sign in with the second factor (happens automatically for server-side browser flows) or change the configuration. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `session_inactive`: No Ory Session was found - sign in a user first. - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserSettingsFlowRequest - */ - CreateBrowserSettingsFlow(ctx context.Context) FrontendApiApiCreateBrowserSettingsFlowRequest - - /* - * CreateBrowserSettingsFlowExecute executes the request - * @return SettingsFlow - */ - CreateBrowserSettingsFlowExecute(r FrontendApiApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) - - /* - * CreateBrowserVerificationFlow Create Verification Flow for Browser Clients - * This endpoint initializes a browser-based account verification flow. Once initialized, the browser will be redirected to - `selfservice.flows.verification.ui_url` with the flow ID set as the query parameter `?flow=`. - - If this endpoint is called via an AJAX request, the response contains the recovery flow without any redirects. - - This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...). - - More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserVerificationFlowRequest - */ - CreateBrowserVerificationFlow(ctx context.Context) FrontendApiApiCreateBrowserVerificationFlowRequest - - /* - * CreateBrowserVerificationFlowExecute executes the request - * @return VerificationFlow - */ - CreateBrowserVerificationFlowExecute(r FrontendApiApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) - - /* - * CreateNativeLoginFlow Create Login Flow for Native Apps - * This endpoint initiates a login flow for native apps that do not use a browser, such as mobile devices, smart TVs, and so on. - - If a valid provided session cookie or session token is provided, a 400 Bad Request error - will be returned unless the URL query parameter `?refresh=true` is set. - - To fetch an existing login flow call `/self-service/login/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks, including CSRF login attacks. - - In the case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `session_aal1_required`: Multi-factor auth (e.g. 2fa) was requested but the user has no session yet. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeLoginFlowRequest - */ - CreateNativeLoginFlow(ctx context.Context) FrontendApiApiCreateNativeLoginFlowRequest - - /* - * CreateNativeLoginFlowExecute executes the request - * @return LoginFlow - */ - CreateNativeLoginFlowExecute(r FrontendApiApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) - - /* - * CreateNativeRecoveryFlow Create Recovery Flow for Native Apps - * This endpoint initiates a recovery flow for API clients such as mobile devices, smart TVs, and so on. - - If a valid provided session cookie or session token is provided, a 400 Bad Request error. - - On an existing recovery flow, use the `getRecoveryFlow` API endpoint. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRecoveryFlowRequest - */ - CreateNativeRecoveryFlow(ctx context.Context) FrontendApiApiCreateNativeRecoveryFlowRequest - - /* - * CreateNativeRecoveryFlowExecute executes the request - * @return RecoveryFlow - */ - CreateNativeRecoveryFlowExecute(r FrontendApiApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) - - /* - * CreateNativeRegistrationFlow Create Registration Flow for Native Apps - * This endpoint initiates a registration flow for API clients such as mobile devices, smart TVs, and so on. - - If a valid provided session cookie or session token is provided, a 400 Bad Request error - will be returned unless the URL query parameter `?refresh=true` is set. - - To fetch an existing registration flow call `/self-service/registration/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - In the case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRegistrationFlowRequest - */ - CreateNativeRegistrationFlow(ctx context.Context) FrontendApiApiCreateNativeRegistrationFlowRequest - - /* - * CreateNativeRegistrationFlowExecute executes the request - * @return RegistrationFlow - */ - CreateNativeRegistrationFlowExecute(r FrontendApiApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) - - /* - * CreateNativeSettingsFlow Create Settings Flow for Native Apps - * This endpoint initiates a settings flow for API clients such as mobile devices, smart TVs, and so on. - You must provide a valid Ory Kratos Session Token for this endpoint to respond with HTTP 200 OK. - - To fetch an existing settings flow call `/self-service/settings/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator - Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn - credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user - to sign in with the second factor or change the configuration. - - In the case of an error, the `error.id` of the JSON response body can be one of: - - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `session_inactive`: No Ory Session was found - sign in a user first. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeSettingsFlowRequest - */ - CreateNativeSettingsFlow(ctx context.Context) FrontendApiApiCreateNativeSettingsFlowRequest - - /* - * CreateNativeSettingsFlowExecute executes the request - * @return SettingsFlow - */ - CreateNativeSettingsFlowExecute(r FrontendApiApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) - - /* - * CreateNativeVerificationFlow Create Verification Flow for Native Apps - * This endpoint initiates a verification flow for API clients such as mobile devices, smart TVs, and so on. - - To fetch an existing verification flow call `/self-service/verification/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeVerificationFlowRequest - */ - CreateNativeVerificationFlow(ctx context.Context) FrontendApiApiCreateNativeVerificationFlowRequest - - /* - * CreateNativeVerificationFlowExecute executes the request - * @return VerificationFlow - */ - CreateNativeVerificationFlowExecute(r FrontendApiApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) - - /* - * DisableMyOtherSessions Disable my other sessions - * Calling this endpoint invalidates all except the current session that belong to the logged-in user. - Session data are not deleted. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiDisableMyOtherSessionsRequest - */ - DisableMyOtherSessions(ctx context.Context) FrontendApiApiDisableMyOtherSessionsRequest - - /* - * DisableMyOtherSessionsExecute executes the request - * @return DeleteMySessionsCount - */ - DisableMyOtherSessionsExecute(r FrontendApiApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) - - /* - * DisableMySession Disable one of my sessions - * Calling this endpoint invalidates the specified session. The current session cannot be revoked. - Session data are not deleted. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param id ID is the session's ID. - * @return FrontendApiApiDisableMySessionRequest - */ - DisableMySession(ctx context.Context, id string) FrontendApiApiDisableMySessionRequest - - /* - * DisableMySessionExecute executes the request - */ - DisableMySessionExecute(r FrontendApiApiDisableMySessionRequest) (*http.Response, error) - - /* - * ExchangeSessionToken Exchange Session Token - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiExchangeSessionTokenRequest - */ - ExchangeSessionToken(ctx context.Context) FrontendApiApiExchangeSessionTokenRequest - - /* - * ExchangeSessionTokenExecute executes the request - * @return SuccessfulNativeLogin - */ - ExchangeSessionTokenExecute(r FrontendApiApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) - - /* - * GetFlowError Get User-Flow Errors - * This endpoint returns the error associated with a user-facing self service errors. - - This endpoint supports stub values to help you implement the error UI: - - `?id=stub:500` - returns a stub 500 (Internal Server Error) error. - - More information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetFlowErrorRequest - */ - GetFlowError(ctx context.Context) FrontendApiApiGetFlowErrorRequest - - /* - * GetFlowErrorExecute executes the request - * @return FlowError - */ - GetFlowErrorExecute(r FrontendApiApiGetFlowErrorRequest) (*FlowError, *http.Response, error) - - /* - * GetLoginFlow Get Login Flow - * This endpoint returns a login flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/login', async function (req, res) { - const flow = await client.getLoginFlow(req.header('cookie'), req.query['flow']) - - res.render('login', flow) - }) - ``` - - This request may fail due to several reasons. The `error.id` can be one of: - - `session_already_available`: The user is already signed in. - `self_service_flow_expired`: The flow is expired and you should request a new one. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetLoginFlowRequest - */ - GetLoginFlow(ctx context.Context) FrontendApiApiGetLoginFlowRequest - - /* - * GetLoginFlowExecute executes the request - * @return LoginFlow - */ - GetLoginFlowExecute(r FrontendApiApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) - - /* - * GetRecoveryFlow Get Recovery Flow - * This endpoint returns a recovery flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/recovery', async function (req, res) { - const flow = await client.getRecoveryFlow(req.header('Cookie'), req.query['flow']) - - res.render('recovery', flow) - }) - ``` - - More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRecoveryFlowRequest - */ - GetRecoveryFlow(ctx context.Context) FrontendApiApiGetRecoveryFlowRequest - - /* - * GetRecoveryFlowExecute executes the request - * @return RecoveryFlow - */ - GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) - - /* - * GetRegistrationFlow Get Registration Flow - * This endpoint returns a registration flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/registration', async function (req, res) { - const flow = await client.getRegistrationFlow(req.header('cookie'), req.query['flow']) - - res.render('registration', flow) - }) - ``` - - This request may fail due to several reasons. The `error.id` can be one of: - - `session_already_available`: The user is already signed in. - `self_service_flow_expired`: The flow is expired and you should request a new one. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRegistrationFlowRequest - */ - GetRegistrationFlow(ctx context.Context) FrontendApiApiGetRegistrationFlowRequest - - /* - * GetRegistrationFlowExecute executes the request - * @return RegistrationFlow - */ - GetRegistrationFlowExecute(r FrontendApiApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) - - /* - * GetSettingsFlow Get Settings Flow - * When accessing this endpoint through Ory Kratos' Public API you must ensure that either the Ory Kratos Session Cookie - or the Ory Kratos Session Token are set. - - Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator - Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn - credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user - to sign in with the second factor or change the configuration. - - You can access this endpoint without credentials when using Ory Kratos' Admin API. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `session_inactive`: No Ory Session was found - sign in a user first. - `security_identity_mismatch`: The flow was interrupted with `session_refresh_required` but apparently some other - identity logged in instead. - - More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetSettingsFlowRequest - */ - GetSettingsFlow(ctx context.Context) FrontendApiApiGetSettingsFlowRequest - - /* - * GetSettingsFlowExecute executes the request - * @return SettingsFlow - */ - GetSettingsFlowExecute(r FrontendApiApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) - - /* - * GetVerificationFlow Get Verification Flow - * This endpoint returns a verification flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/recovery', async function (req, res) { - const flow = await client.getVerificationFlow(req.header('cookie'), req.query['flow']) - - res.render('verification', flow) - }) - ``` - - More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetVerificationFlowRequest - */ - GetVerificationFlow(ctx context.Context) FrontendApiApiGetVerificationFlowRequest - - /* - * GetVerificationFlowExecute executes the request - * @return VerificationFlow - */ - GetVerificationFlowExecute(r FrontendApiApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) - - /* - * GetWebAuthnJavaScript Get WebAuthn JavaScript - * This endpoint provides JavaScript which is needed in order to perform WebAuthn login and registration. - - If you are building a JavaScript Browser App (e.g. in ReactJS or AngularJS) you will need to load this file: - - ```html -