From ff483130bd412049a2111e4bee129eb988909ab4 Mon Sep 17 00:00:00 2001 From: Stefan Jacobi Date: Mon, 26 Feb 2024 11:58:29 +0100 Subject: [PATCH] feat(webauthn): allow optional user_id for login/initialize * add login init dto with user_id as optional param * extend service to switch to BeginLogin /ValidateLogin when user_id was given * extend database to persist login state in sessiondata for login/finalize * extend openapi spec to reflect optional user_id parameter for login/initialize Closes: #33 --- .../api/dto/intern/webauthn_session_data.go | 3 +- server/api/dto/request/requests.go | 6 +- server/api/handler/login.go | 7 +++ server/api/services/login_service.go | 60 ++++++++++++++----- server/api/services/registration_service.go | 2 +- server/api/services/transaction_service.go | 2 +- server/api/services/webauthn_service.go | 1 + ..._add_discoverable_to_sessiondata.down.fizz | 1 + ...25_add_discoverable_to_sessiondata.up.fizz | 1 + .../models/webauthn_session_data.go | 3 +- spec/passkey-server.yaml | 12 ++++ 11 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.down.fizz create mode 100644 server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.up.fizz diff --git a/server/api/dto/intern/webauthn_session_data.go b/server/api/dto/intern/webauthn_session_data.go index 62e451f..414b7ac 100644 --- a/server/api/dto/intern/webauthn_session_data.go +++ b/server/api/dto/intern/webauthn_session_data.go @@ -33,7 +33,7 @@ func WebauthnSessionDataFromModel(data *models.WebauthnSessionData) *webauthn.Se } } -func WebauthnSessionDataToModel(data *webauthn.SessionData, tenantId uuid.UUID, operation models.Operation) *models.WebauthnSessionData { +func WebauthnSessionDataToModel(data *webauthn.SessionData, tenantId uuid.UUID, operation models.Operation, isDiscoverable bool) *models.WebauthnSessionData { id, _ := uuid.NewV4() now := time.Now() @@ -62,5 +62,6 @@ func WebauthnSessionDataToModel(data *webauthn.SessionData, tenantId uuid.UUID, AllowedCredentials: allowedCredentials, ExpiresAt: nulls.NewTime(data.Expires), TenantID: tenantId, + IsDiscoverable: isDiscoverable, } } diff --git a/server/api/dto/request/requests.go b/server/api/dto/request/requests.go index 0b15bdd..82473e2 100644 --- a/server/api/dto/request/requests.go +++ b/server/api/dto/request/requests.go @@ -30,7 +30,7 @@ type UpdateCredentialsDto struct { } type WebauthnRequests interface { - InitRegistrationDto | InitTransactionDto + InitRegistrationDto | InitTransactionDto | InitLoginDto } type InitRegistrationDto struct { @@ -91,3 +91,7 @@ func (initTransaction *InitTransactionDto) ToModel() (*models.Transaction, error UpdatedAt: now, }, nil } + +type InitLoginDto struct { + UserId *string `json:"user_id" validate:"omitempty,min=1"` +} diff --git a/server/api/handler/login.go b/server/api/handler/login.go index c1d70ee..5dad6d9 100644 --- a/server/api/handler/login.go +++ b/server/api/handler/login.go @@ -5,6 +5,7 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/gobuffalo/pop/v6" "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/api/dto/request" "github.com/teamhanko/passkey-server/api/dto/response" "github.com/teamhanko/passkey-server/api/helper" "github.com/teamhanko/passkey-server/api/services" @@ -33,6 +34,11 @@ func (lh *loginHandler) Init(ctx echo.Context) error { return err } + dto, err := BindAndValidateRequest[request.InitLoginDto](ctx) + if err != nil { + return err + } + return lh.persister.GetConnection().Transaction(func(tx *pop.Connection) error { userPersister := lh.persister.GetWebauthnUserPersister(tx) sessionPersister := lh.persister.GetWebauthnSessionDataPersister(tx) @@ -42,6 +48,7 @@ func (lh *loginHandler) Init(ctx echo.Context) error { Ctx: ctx, Tenant: *h.Tenant, WebauthnClient: *h.Webauthn, + UserId: dto.UserId, UserPersister: userPersister, SessionPersister: sessionPersister, CredentialPersister: credentialPersister, diff --git a/server/api/services/login_service.go b/server/api/services/login_service.go index 5ff4425..6febd5e 100644 --- a/server/api/services/login_service.go +++ b/server/api/services/login_service.go @@ -18,6 +18,7 @@ type LoginService interface { type loginService struct { WebauthnService + userId *string } func NewLoginService(params WebauthnServiceCreateParams) LoginService { @@ -33,23 +34,48 @@ func NewLoginService(params WebauthnServiceCreateParams) LoginService { userPersister: params.UserPersister, sessionDataPersister: params.SessionPersister, - }} + }, + params.UserId} } func (ls *loginService) Initialize() (*protocol.CredentialAssertion, error) { - credentialAssertion, sessionData, err := ls.webauthnClient.BeginDiscoverableLogin( - webauthn.WithUserVerification(ls.tenant.Config.WebauthnConfig.UserVerification), - ) - - if err != nil { - ls.logger.Error(err) - return nil, echo.NewHTTPError( - http.StatusInternalServerError, - fmt.Errorf("failed to create webauthn assertion options for discoverable login: %w", err), + var credentialAssertion *protocol.CredentialAssertion + var sessionData *webauthn.SessionData + var err error + isDiscoverable := true + + if ls.userId != nil { + user, err := ls.getWebauthnUserByUserHandle(*ls.userId) + if err != nil { + ls.logger.Error(err) + + return nil, echo.NewHTTPError(http.StatusNotFound, err) + } + + credentialAssertion, sessionData, err = ls.webauthnClient.BeginLogin(user, webauthn.WithUserVerification(ls.tenant.Config.WebauthnConfig.UserVerification)) + if err != nil { + ls.logger.Error(err) + return nil, echo.NewHTTPError( + http.StatusInternalServerError, + fmt.Errorf("failed to create webauthn assertion options for login: %w", err), + ) + } + isDiscoverable = false + } else { + credentialAssertion, sessionData, err = ls.webauthnClient.BeginDiscoverableLogin( + webauthn.WithUserVerification(ls.tenant.Config.WebauthnConfig.UserVerification), ) + + if err != nil { + ls.logger.Error(err) + return nil, echo.NewHTTPError( + http.StatusInternalServerError, + fmt.Errorf("failed to create webauthn assertion options for discoverable login: %w", err), + ) + } } - err = ls.sessionDataPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, ls.tenant.ID, models.WebauthnOperationAuthentication)) + err = ls.sessionDataPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, ls.tenant.ID, models.WebauthnOperationAuthentication, isDiscoverable)) if err != nil { ls.logger.Error(err) return nil, err @@ -79,9 +105,15 @@ func (ls *loginService) Finalize(req *protocol.ParsedCredentialAssertionData) (s return "", userHandle, echo.NewHTTPError(http.StatusUnauthorized, "failed to get user handle").SetInternal(err) } - credential, err := ls.webauthnClient.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) { - return webauthnUser, nil - }, *sessionData, req) + var credential *webauthn.Credential + if dbSessionData.IsDiscoverable { + credential, err = ls.webauthnClient.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) { + return webauthnUser, nil + }, *sessionData, req) + } else { + credential, err = ls.webauthnClient.ValidateLogin(webauthnUser, *sessionData, req) + } + if err != nil { ls.logger.Error(err) return "", userHandle, echo.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err) diff --git a/server/api/services/registration_service.go b/server/api/services/registration_service.go index 0328259..63921ec 100644 --- a/server/api/services/registration_service.go +++ b/server/api/services/registration_service.go @@ -70,7 +70,7 @@ func (rs *registrationService) Initialize(user *models.WebauthnUser) (*protocol. return nil, internalUser.UserId, err } - err = rs.sessionDataPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, rs.tenant.ID, models.WebauthnOperationRegistration)) + err = rs.sessionDataPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, rs.tenant.ID, models.WebauthnOperationRegistration, false)) if err != nil { return nil, internalUser.UserId, err } diff --git a/server/api/services/transaction_service.go b/server/api/services/transaction_service.go index 84e5d86..4e00d83 100644 --- a/server/api/services/transaction_service.go +++ b/server/api/services/transaction_service.go @@ -92,7 +92,7 @@ func (ts *transactionService) Initialize(userId string, transaction *models.Tran return nil, err } - err = ts.sessionDataPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, ts.tenant.ID, models.WebauthnOperationTransaction)) + err = ts.sessionDataPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, ts.tenant.ID, models.WebauthnOperationTransaction, false)) if err != nil { ts.logger.Error(err) return nil, err diff --git a/server/api/services/webauthn_service.go b/server/api/services/webauthn_service.go index 2872bcd..8a25b13 100644 --- a/server/api/services/webauthn_service.go +++ b/server/api/services/webauthn_service.go @@ -31,6 +31,7 @@ type WebauthnServiceCreateParams struct { WebauthnClient webauthn.WebAuthn Generator jwt.Generator AuthenticatorMetadata mapper.AuthenticatorMetadata + UserId *string UserPersister persisters.WebauthnUserPersister SessionPersister persisters.WebauthnSessionDataPersister diff --git a/server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.down.fizz b/server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.down.fizz new file mode 100644 index 0000000..b86bb84 --- /dev/null +++ b/server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.down.fizz @@ -0,0 +1 @@ +drop_column("webauthn_session_data", "is_discoverable") diff --git a/server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.up.fizz b/server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.up.fizz new file mode 100644 index 0000000..b3e5409 --- /dev/null +++ b/server/persistence/migrations/20240226103525_add_discoverable_to_sessiondata.up.fizz @@ -0,0 +1 @@ +add_column("webauthn_session_data", "is_discoverable", "bool", { "default": true }) diff --git a/server/persistence/models/webauthn_session_data.go b/server/persistence/models/webauthn_session_data.go index 2f73368..669e61b 100644 --- a/server/persistence/models/webauthn_session_data.go +++ b/server/persistence/models/webauthn_session_data.go @@ -29,13 +29,14 @@ type WebauthnSessionData struct { Operation Operation `db:"operation"` AllowedCredentials []WebauthnSessionDataAllowedCredential `has_many:"webauthn_session_data_allowed_credentials"` ExpiresAt nulls.Time `db:"expires_at"` + IsDiscoverable bool `db:"is_discoverable"` TenantID uuid.UUID `db:"tenant_id"` Tenant *Tenant `belongs_to:"tenants"` } // Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. -func (sd *WebauthnSessionData) Validate(tx *pop.Connection) (*validate.Errors, error) { +func (sd *WebauthnSessionData) Validate(_ *pop.Connection) (*validate.Errors, error) { return validate.Validate( &validators.UUIDIsPresent{Name: "ID", Field: sd.ID}, &validators.StringIsPresent{Name: "Challenge", Field: sd.Challenge}, diff --git a/spec/passkey-server.yaml b/spec/passkey-server.yaml index 5a9dde8..ac76e1a 100644 --- a/spec/passkey-server.yaml +++ b/spec/passkey-server.yaml @@ -176,6 +176,8 @@ paths: operationId: post-login-initialize parameters: - $ref: '#/components/parameters/tenant_id' + requestBody: + $ref: '#/components/requestBodies/post-login-initialize' responses: '200': $ref: '#/components/responses/post-login-initialize' @@ -428,6 +430,16 @@ components: - user_id - transaction_id - transaction_data + post-login-initialize: + description: Body for login/initialize + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + description: optional responses: get-credentials: description: Example response