Files
upstream-ory-kratos/selfservice/strategy/passkey/passkey_schema_extension.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

116 lines
3.5 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package passkey
import (
"cmp"
"context"
"fmt"
"slices"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/ory/jsonschema/v3"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/x/jsonschemax"
)
// errNoDisplayNameTrait is returned by PasskeyDisplayNameFromSchema when the
// identity schema flags no trait as a passkey display name or WebAuthn
// identifier and has no untitled trait to fall back to.
var errNoDisplayNameTrait = errors.New("no identifier found")
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()
// When a schema flags multiple traits, the validator visits them in an
// unspecified order. Keep the first non-empty value so an empty flagged
// trait never clobbers a populated one. This mirrors the client, which
// names the passkey after the first non-empty candidate field.
if s.Credentials.WebAuthn.Identifier && e.WebauthnIdentifier == "" {
e.WebauthnIdentifier = strings.ToLower(fmt.Sprintf("%s", value))
}
if s.Credentials.Passkey.DisplayName && e.PasskeyDisplayName == "" {
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 cmp.Or(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)
}
// PasskeyDisplayNameFromSchema returns every trait path (e.g. ["traits.email", "traits.phone"])
// whose schema flags `passkey.display_name: true` or `webauthn.identifier: true`. The slice
// is sorted alphabetically so server and client agree on precedence. When no
// trait is flagged, it preserves the legacy behavior: fall back to the first
// untitled trait, and return an error only when there is none.
func (s *Strategy) PasskeyDisplayNameFromSchema(ctx context.Context, schemaURL string) ([]string, error) {
runner, err := schema.NewExtensionRunner(ctx)
if err != nil {
return nil, err
}
c, err := schema.NewCompilerWithURL(ctx, schemaURL, s.d.Config().SecurityDisallowRefInIdentitySchemas(ctx))
if err != nil {
return nil, err
}
c.ExtractAnnotations = true
runner.Register(c)
paths, err := jsonschemax.ListPaths(ctx, schemaURL, c)
if err != nil {
return nil, err
}
var result []string
for _, p := range paths {
ext, ok := p.CustomProperties[schema.ExtensionName].(*schema.ExtensionConfig)
if !ok {
continue
}
if ext.Credentials.WebAuthn.Identifier || ext.Credentials.Passkey.DisplayName {
result = append(result, p.Name)
}
}
if len(result) > 0 {
slices.Sort(result)
return result, nil
}
for _, p := range paths {
if strings.HasPrefix(p.Name, "traits.") && p.Title == "" {
return []string{p.Name}, nil
}
}
return nil, errors.WithStack(errNoDisplayNameTrait)
}