diff --git a/.gitignore b/.gitignore index cf6eaa00e6a0..fdf1a69a8613 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ cover.out .idea/ tmp/ .DS_Store -./kratos +/kratos packrd/ *-packr.go dist/ @@ -60,7 +60,5 @@ test/e2e/hydra-kratos-login-consent/hydra-kratos-login-consent test/e2e/proxy.json test/e2e/kratos.*.yml -# Kratos executable -kratos # VSCode debug artifact -__debug_bin \ No newline at end of file +__debug_bin diff --git a/contrib/quickstart/kratos/webauthn/identity.schema.json b/contrib/quickstart/kratos/webauthn/identity.schema.json new file mode 100644 index 000000000000..2734e5c88a96 --- /dev/null +++ b/contrib/quickstart/kratos/webauthn/identity.schema.json @@ -0,0 +1,50 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/webauthn/kratos.yml b/contrib/quickstart/kratos/webauthn/kratos.yml new file mode 100644 index 000000000000..7070aa4bcc09 --- /dev/null +++ b/contrib/quickstart/kratos/webauthn/kratos.yml @@ -0,0 +1,107 @@ +version: v0.11.0 + +dsn: memory + +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://localhost:4455/ + allowed_return_urls: + - http://localhost:4455 + + methods: + password: + enabled: false + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + webauthn: + config: + passwordless: true + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origin: http://localhost:4455 + enabled: true + + flows: + error: + ui_url: http://localhost:4455/error + + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://localhost:4455/recovery + use: code + + verification: + enabled: true + ui_url: http://localhost:4455/verification + use: code + after: + default_browser_return_url: http://localhost:4455/ + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + - hook: show_verification_ui + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: default + schemas: + - id: default + url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/quickstart-webauthn.yml b/quickstart-webauthn.yml new file mode 100644 index 000000000000..d762ca124b06 --- /dev/null +++ b/quickstart-webauthn.yml @@ -0,0 +1,18 @@ +version: "3.7" + +services: + kratos-migrate: + volumes: + - type: bind + source: ./contrib/quickstart/kratos/webauthn + target: /etc/config/kratos + + kratos: + volumes: + - type: bind + source: ./contrib/quickstart/kratos/webauthn + target: /etc/config/kratos + + kratos-selfservice-ui-node: + environment: + - KRATOS_BROWSER_URL=http://localhost:4433/ diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 12050ddb00b6..33e2689ea315 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -119,16 +119,16 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident } if len(webAuthCreds) == 0 { - // Identity has no webauth + // Identity has no webauthn return ErrNoCredentials } - web, err := s.newWebAuthn(r.Context()) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate WebAuth.").WithDebug(err.Error())) } - options, sessionData, err := web.BeginLogin(&wrappedUser{id: conf.UserHandle, c: webAuthCreds}) + options, sessionData, err := web.BeginLogin(NewUser(conf.UserHandle, webAuthCreds, web.Config)) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate WebAuth login.").WithDebug(err.Error())) } @@ -295,7 +295,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("The WebAuthn credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err))) } - web, err := s.newWebAuthn(r.Context()) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) if err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) } @@ -315,7 +315,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * webAuthCreds = o.Credentials.ToWebAuthn() } - if _, err := web.ValidateLogin(&wrappedUser{id: o.UserHandle, c: webAuthCreds}, webAuthnSess, webAuthnResponse); err != nil { + if _, err := web.ValidateLogin(NewUser(o.UserHandle, webAuthCreds, web.Config), webAuthnSess, webAuthnResponse); err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/"))) } diff --git a/selfservice/strategy/webauthn/mock.go b/selfservice/strategy/webauthn/mock.go deleted file mode 100644 index d7c84b0c6509..000000000000 --- a/selfservice/strategy/webauthn/mock.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package webauthn - -import ( - "github.com/duo-labs/webauthn/webauthn" -) - -type wrappedUser struct { - id []byte - c []webauthn.Credential -} - -var _ webauthn.User = (*wrappedUser)(nil) - -func (user *wrappedUser) WebAuthnID() []byte { - return user.id -} - -func (user *wrappedUser) WebAuthnName() string { - return "placeholder" -} - -func (user *wrappedUser) WebAuthnDisplayName() string { - return "placeholder" -} - -func (user *wrappedUser) WebAuthnIcon() string { - return "https://via.placeholder.com/128" -} - -func (user *wrappedUser) WebAuthnCredentials() []webauthn.Credential { - return user.c -} diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go index 79186f7f4269..c66e015e125b 100644 --- a/selfservice/strategy/webauthn/registration.go +++ b/selfservice/strategy/webauthn/registration.go @@ -137,12 +137,12 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err))) } - web, err := s.newWebAuthn(r.Context()) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) if err != nil { return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) } - credential, err := web.CreateCredential(&wrappedUser{id: webAuthnSess.UserID}, webAuthnSess, webAuthnResponse) + credential, err := web.CreateCredential(NewUser(webAuthnSess.UserID, nil, web.Config), webAuthnSess, webAuthnResponse) if err != nil { return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err))) } @@ -197,13 +197,14 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.F f.UI.SetNode(n) } - web, err := s.newWebAuthn(r.Context()) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) if err != nil { - return err + return errors.WithStack(err) } webauthID := x.NewUUID() - option, sessionData, err := web.BeginRegistration(&wrappedUser{id: webauthID[:]}) + user := NewUser(webauthID[:], nil, s.d.Config().WebAuthnConfig(r.Context())) + option, sessionData, err := web.BeginRegistration(user) if err != nil { return errors.WithStack(err) } diff --git a/selfservice/strategy/webauthn/settings.go b/selfservice/strategy/webauthn/settings.go index 6ecfa36b84c0..a6e513a20c99 100644 --- a/selfservice/strategy/webauthn/settings.go +++ b/selfservice/strategy/webauthn/settings.go @@ -242,12 +242,12 @@ func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Reques return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err)) } - web, err := s.newWebAuthn(r.Context()) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())) } - credential, err := web.CreateCredential(&wrappedUser{id: ctxUpdate.Session.IdentityID[:]}, webAuthnSess, webAuthnResponse) + credential, err := web.CreateCredential(NewUser(ctxUpdate.Session.IdentityID[:], nil, web.Config), webAuthnSess, webAuthnResponse) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err)) } @@ -356,12 +356,12 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity } } - web, err := s.newWebAuthn(r.Context()) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) if err != nil { - return err + return errors.WithStack(err) } - option, sessionData, err := web.BeginRegistration(&wrappedUser{id: id.ID[:]}) + option, sessionData, err := web.BeginRegistration(NewUser(id.ID[:], nil, web.Config)) if err != nil { return errors.WithStack(err) } diff --git a/selfservice/strategy/webauthn/strategy.go b/selfservice/strategy/webauthn/strategy.go index 0f3bfe44a1b7..051267efc9ba 100644 --- a/selfservice/strategy/webauthn/strategy.go +++ b/selfservice/strategy/webauthn/strategy.go @@ -7,8 +7,6 @@ import ( "context" "encoding/json" - "github.com/duo-labs/webauthn/webauthn" - "github.com/pkg/errors" "github.com/ory/kratos/continuity" @@ -114,16 +112,6 @@ func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.WebAuthnGroup } -func (s *Strategy) newWebAuthn(ctx context.Context) (*webauthn.WebAuthn, error) { - c := s.d.Config() - web, err := webauthn.New(c.WebAuthnConfig(ctx)) - if err != nil { - return nil, errors.WithStack(err) - } - - return web, nil -} - func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { aal := identity.AuthenticatorAssuranceLevel1 if !s.d.Config().WebAuthnForPasswordless(ctx) { diff --git a/selfservice/strategy/webauthn/user.go b/selfservice/strategy/webauthn/user.go new file mode 100644 index 000000000000..4d644afbe7fd --- /dev/null +++ b/selfservice/strategy/webauthn/user.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthn + +import "github.com/duo-labs/webauthn/webauthn" + +var _ webauthn.User = (*User)(nil) + +type User struct { + id []byte + c []webauthn.Credential + cfg *webauthn.Config +} + +func NewUser(id []byte, c []webauthn.Credential, cfg *webauthn.Config) *User { + return &User{ + id: id, + c: c, + cfg: cfg, + } +} + +func (u *User) WebAuthnID() []byte { + return u.id +} + +func (u *User) WebAuthnName() string { + return u.cfg.RPDisplayName +} + +func (u *User) WebAuthnDisplayName() string { + return u.cfg.RPDisplayName +} + +func (u *User) WebAuthnIcon() string { + return u.cfg.RPIcon +} + +func (u *User) WebAuthnCredentials() []webauthn.Credential { + return u.c +}