Skip to content

Commit

Permalink
feat(webauthn): allow optional user_id for login/initialize
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Stefan Jacobi committed Mar 5, 2024
1 parent ccdac94 commit ff48313
Show file tree
Hide file tree
Showing 11 changed files with 79 additions and 19 deletions.
3 changes: 2 additions & 1 deletion server/api/dto/intern/webauthn_session_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -62,5 +62,6 @@ func WebauthnSessionDataToModel(data *webauthn.SessionData, tenantId uuid.UUID,
AllowedCredentials: allowedCredentials,
ExpiresAt: nulls.NewTime(data.Expires),
TenantID: tenantId,
IsDiscoverable: isDiscoverable,
}
}
6 changes: 5 additions & 1 deletion server/api/dto/request/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type UpdateCredentialsDto struct {
}

type WebauthnRequests interface {
InitRegistrationDto | InitTransactionDto
InitRegistrationDto | InitTransactionDto | InitLoginDto
}

type InitRegistrationDto struct {
Expand Down Expand Up @@ -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"`
}
7 changes: 7 additions & 0 deletions server/api/handler/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
60 changes: 46 additions & 14 deletions server/api/services/login_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type LoginService interface {

type loginService struct {
WebauthnService
userId *string
}

func NewLoginService(params WebauthnServiceCreateParams) LoginService {
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion server/api/services/registration_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion server/api/services/transaction_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server/api/services/webauthn_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type WebauthnServiceCreateParams struct {
WebauthnClient webauthn.WebAuthn
Generator jwt.Generator
AuthenticatorMetadata mapper.AuthenticatorMetadata
UserId *string

UserPersister persisters.WebauthnUserPersister
SessionPersister persisters.WebauthnSessionDataPersister
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
drop_column("webauthn_session_data", "is_discoverable")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add_column("webauthn_session_data", "is_discoverable", "bool", { "default": true })
3 changes: 2 additions & 1 deletion server/persistence/models/webauthn_session_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
12 changes: 12 additions & 0 deletions spec/passkey-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ff48313

Please sign in to comment.