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
341 lines
11 KiB
Go
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)
|
|
}
|