Files
upstream-ory-kratos/identity/handler_import.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

341 lines
11 KiB
Go

// 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)
}