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
116 lines
3.5 KiB
Go
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)
|
|
}
|