// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package identity import ( "bytes" "context" "encoding/json" "slices" "github.com/pkg/errors" "github.com/ory/herodot" "github.com/ory/kratos/hash" ) func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *IdentityWithCredentials) error { if creds == nil { return nil } // If other methods are added please ensure that the available AAL is set correctly in the identity. // // It would actually be good if we would validate the identity post-creation to see if the credentials are working. if creds.Password != nil { // This method is somewhat hacky, because it does not set the credential's identifier. It relies on the // identity validation to set the identifier, which is called after this method. // // It would be good to make this explicit. if err := h.ImportPasswordCredentials(ctx, i, creds.Password); err != nil { return err } } if creds.OIDC != nil { if err := h.importOIDCCredentials(ctx, i, creds.OIDC); err != nil { return err } } if creds.SAML != nil { if err := h.importSAMLCredentials(ctx, i, creds.SAML); err != nil { return err } } if creds.WebAuthn != nil { if err := h.importWebAuthnCredentials(ctx, i, creds.WebAuthn.Config.Credentials, creds.WebAuthn.Config.UserHandle); err != nil { return err } } if creds.Passkey != nil { if err := h.importPasskeyCredentials(ctx, i, creds.Passkey.Config.Credentials, creds.Passkey.Config.UserHandle); err != nil { return err } } if creds.LookupSecret != nil { if err := h.importLookupSecretCredentials(ctx, i, creds.LookupSecret); err != nil { return err } } if creds.TOTP != nil { if err := h.importTOTPCredentials(ctx, i, creds.TOTP); err != nil { return err } } return nil } func (h *Handler) importWebAuthnCredentials(_ context.Context, i *Identity, newCredentials CredentialsWebAuthn, userHandle []byte) error { resultCredentials, previousUserHandle, previousIdentifiers, err := mergeWebAuthnCredentials(i, CredentialsTypeWebAuthn, newCredentials) if err != nil { return err } if len(userHandle) == 0 { userHandle = previousUserHandle } return i.SetCredentialsWithConfig( CredentialsTypeWebAuthn, Credentials{ Identifiers: previousIdentifiers, }, CredentialsWebAuthnConfig{ Credentials: resultCredentials, UserHandle: userHandle, }, ) } // mergeWebAuthnCredentials merges existing WebAuthn credentials with new ones. // It updates credentials with the same ID and appends new ones. // Returns the merged credentials, previous user handle, previous identifiers, and any error. func mergeWebAuthnCredentials( i *Identity, credentialType CredentialsType, newCredentials CredentialsWebAuthn, ) (CredentialsWebAuthn, []byte, []string, error) { var previousUserHandle []byte var previousIdentifiers []string resultCredentials := newCredentials if c, ok := i.GetCredentials(credentialType); ok { var original CredentialsWebAuthnConfig if err := json.Unmarshal(c.Config, &original); err != nil { return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) } resultCredentials = make(CredentialsWebAuthn, len(original.Credentials)) copy(resultCredentials, original.Credentials) for _, updated := range newCredentials { var found bool for k, originalCredential := range original.Credentials { if bytes.Equal(originalCredential.ID, updated.ID) { resultCredentials[k] = updated found = true break } } if !found { resultCredentials = append(resultCredentials, updated) } } previousUserHandle = original.UserHandle previousIdentifiers = c.Identifiers } return resultCredentials, previousUserHandle, previousIdentifiers, nil } func (h *Handler) importPasskeyCredentials(_ context.Context, i *Identity, newCredentials CredentialsWebAuthn, userHandle []byte) error { resultCredentials, previousUserHandle, previousIdentifiers, err := mergeWebAuthnCredentials(i, CredentialsTypePasskey, newCredentials) if err != nil { return err } identifiers := previousIdentifiers if len(userHandle) == 0 { userHandle = previousUserHandle } else if !slices.Contains(identifiers, string(userHandle)) { identifiers = append(identifiers, string(userHandle)) } // The passkey login flow looks the credential up by joining // identity_credential_identifiers on the userHandle returned by the // authenticator during the WebAuthn assertion (see // selfservice/strategy/passkey/passkey_login.go). The userHandle is // generated at registration time and bound to the physical authenticator, // so the server cannot reconstruct or default it: an import has to // supply it, or inherit it from a previously persisted passkey // credential. Without a userHandle the credential row would be // non-functional — no assertion can ever resolve to it. Reject the // import early instead of silently storing a broken credential. See // https://github.com/ory/kratos/issues/4561. if len(userHandle) == 0 { return errors.WithStack(herodot.ErrBadRequest().WithReasonf( "Passkey credential import requires a non-empty user_handle. " + "It must match the WebAuthn user handle stored on the authenticator.")) } if len(resultCredentials) == 0 { return errors.WithStack(herodot.ErrBadRequest().WithReasonf( "Passkey credential import requires at least one entry in credentials.passkey.config.credentials.")) } return i.SetCredentialsWithConfig( CredentialsTypePasskey, Credentials{ Identifiers: identifiers, }, CredentialsWebAuthnConfig{ Credentials: resultCredentials, UserHandle: userHandle, }, ) } func (h *Handler) importLookupSecretCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsLookupSecret) error { var codes []RecoveryCode if c, ok := i.GetCredentials(CredentialsTypeLookup); ok { var target CredentialsLookupConfig if err := json.Unmarshal(c.Config, &target); err != nil { return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) } codes = target.RecoveryCodes } return i.SetCredentialsWithConfig( CredentialsTypeLookup, Credentials{}, CredentialsLookupConfig{ RecoveryCodes: append(codes, creds.Config.Codes...), }, ) } func (h *Handler) importTOTPCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsTOTP) error { // The TOTP login flow resolves the credential by joining // identity_credential_identifiers, using the identity ID as the identifier // (see selfservice/strategy/totp/login.go and settings.go). On imports of // new identities the identity ID is not yet known here, so we leave // Identifiers empty and let the persister default it from the persisted // identity ID. See https://github.com/ory/kratos/issues/4561. return i.SetCredentialsWithConfig( CredentialsTypeTOTP, Credentials{}, CredentialsTOTPConfig{TOTPURL: creds.Config.TOTPURL}, ) } func (h *Handler) ImportPasswordCredentials(ctx context.Context, i *Identity, creds *AdminIdentityImportCredentialsPassword) (err error) { if creds.Config.UsePasswordMigrationHook { return i.SetCredentialsWithConfig(CredentialsTypePassword, Credentials{}, CredentialsPassword{UsePasswordMigrationHook: true}) } // In here we deliberately ignore any password policies as the point here is to import passwords, even if they // are not matching the policy, as the user needs to able to sign in with their old password. hashed := []byte(creds.Config.HashedPassword) if len(creds.Config.Password) > 0 { // Importing a clear text password hashed, err = h.r.Hasher(ctx).Generate(ctx, []byte(creds.Config.Password)) if err != nil { return err } creds.Config.HashedPassword = string(hashed) } if !(hash.IsValidHashFormat(hashed)) { return errors.WithStack(herodot.ErrBadRequest().WithReason("The imported password does not match any known hash format.")) } if err := hash.ValidateImportedHash(hashed); err != nil { if errors.Is(err, hash.ErrHashParametersOutOfBounds) { return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason( "The imported password hash declares cost parameters that exceed the bounds enforced by Ory. " + "Re-hash the password with a more conservative configuration before importing.")) } return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason( "The imported password hash could not be parsed.")) } return i.SetCredentialsWithConfig(CredentialsTypePassword, Credentials{}, CredentialsPassword{HashedPassword: string(hashed)}) } func (h *Handler) importOIDCCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsOIDC) error { var target CredentialsOIDC c, ok := i.GetCredentials(CredentialsTypeOIDC) if !ok { var providers []CredentialsOIDCProvider var ids []string for _, p := range creds.Config.Providers { ids = append(ids, OIDCUniqueID(p.Provider, p.Subject)) provider := CredentialsOIDCProvider{ Subject: p.Subject, Provider: p.Provider, UseAutoLink: p.UseAutoLink, } if p.Organization.Valid { provider.Organization = p.Organization.UUID.String() } providers = append(providers, provider) } return i.SetCredentialsWithConfig( CredentialsTypeOIDC, Credentials{Identifiers: ids}, CredentialsOIDC{Providers: providers}, ) } if err := json.Unmarshal(c.Config, &target); err != nil { return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) } for _, p := range creds.Config.Providers { c.Identifiers = append(c.Identifiers, OIDCUniqueID(p.Provider, p.Subject)) provider := CredentialsOIDCProvider{ Subject: p.Subject, Provider: p.Provider, UseAutoLink: p.UseAutoLink, } if p.Organization.Valid { provider.Organization = p.Organization.UUID.String() } target.Providers = append(target.Providers, provider) } return i.SetCredentialsWithConfig(CredentialsTypeOIDC, *c, &target) } func (h *Handler) importSAMLCredentials(_ context.Context, i *Identity, creds *AdminIdentityImportCredentialsSAML) error { var target CredentialsOIDC c, ok := i.GetCredentials(CredentialsTypeSAML) if !ok { var providers []CredentialsOIDCProvider var ids []string for _, p := range creds.Config.Providers { ids = append(ids, OIDCUniqueID(p.Provider, p.Subject)) provider := CredentialsOIDCProvider{ Subject: p.Subject, Provider: p.Provider, } if p.Organization.Valid { provider.Organization = p.Organization.UUID.String() } providers = append(providers, provider) } return i.SetCredentialsWithConfig( CredentialsTypeSAML, Credentials{Identifiers: ids}, CredentialsOIDC{Providers: providers}, ) } if err := json.Unmarshal(c.Config, &target); err != nil { return errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReason(err.Error())) } for _, p := range creds.Config.Providers { c.Identifiers = append(c.Identifiers, OIDCUniqueID(p.Provider, p.Subject)) provider := CredentialsOIDCProvider{ Subject: p.Subject, Provider: p.Provider, } if p.Organization.Valid { provider.Organization = p.Organization.UUID.String() } target.Providers = append(target.Providers, provider) } return i.SetCredentialsWithConfig(CredentialsTypeSAML, *c, &target) }