snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
340
identity/handler_import.go
Normal file
340
identity/handler_import.go
Normal file
@@ -0,0 +1,340 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user