Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webauthn): make webauthn params configurable #48

Merged
merged 13 commits into from
Apr 24, 2024
16 changes: 12 additions & 4 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ curl --location 'http://<YOUR DOMAIN>:8001/tenants' \
]
},
"timeout": 60000,
"user_verification": "preferred"
"user_verification": "preferred",
"attestation_preference": "none",
"resident_key_requirement": "required"
},
"create_api_key": true
}
Expand Down Expand Up @@ -225,7 +227,9 @@ the [WebAuthn Relying Party](https://www.w3.org/TR/webauthn-2/#webauthn-relying-
]
},
"timeout": 60000,
"user_verification": "preferred"
"user_verification": "preferred",
"attestation_preference": "none",
"resident_key_requirement": "required"
}
}
}
Expand All @@ -252,7 +256,9 @@ As an example: If the login should be available at `https://login.example.com` i
]
},
"timeout": 60000,
"user_verification": "preferred"
"user_verification": "preferred",
"attestation_preference": "none",
"resident_key_requirement": "required"
}
}
}
Expand All @@ -275,7 +281,9 @@ point. Then the WebAuthn config would look like this:
]
},
"timeout": 60000,
"user_verification": "preferred"
"user_verification": "preferred",
"attestation_preference": "none",
"resident_key_requirement": "required"
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions server/api/dto/admin/request/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
)

type CreateConfigDto struct {
Cors CreateCorsDto `json:"cors" validate:"required"`
Webauthn CreateWebauthnDto `json:"webauthn" validate:"required"`
Cors CreateCorsDto `json:"cors" validate:"required"`
Webauthn CreateWebauthnConfigDto `json:"webauthn" validate:"required"`
Mfa CreateWebauthnConfigDto `json:"mfa" validate:"required"`
}

func (dto *CreateConfigDto) ToModel(tenant models.Tenant) models.Config {
Expand Down
81 changes: 70 additions & 11 deletions server/api/dto/admin/request/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,83 @@ import (
"time"
)

type CreateWebauthnDto struct {
RelyingParty CreateRelyingPartyDto `json:"relying_party" validate:"required"`
Timeout int `json:"timeout" validate:"required,number"`
UserVerification protocol.UserVerificationRequirement `json:"user_verification" validate:"required,oneof=required preferred discouraged"`
type CreateWebauthnConfigDto struct {
shentschel marked this conversation as resolved.
Show resolved Hide resolved
RelyingParty CreateRelyingPartyDto `json:"relying_party" validate:"required"`
Timeout int `json:"timeout" validate:"required,number"`
UserVerification *protocol.UserVerificationRequirement `json:"user_verification" validate:"omitempty,oneof=required preferred discouraged"`
Attachment *protocol.AuthenticatorAttachment `json:"attachment" validate:"omitempty,oneof=platform cross-platform"`
AttestationPreference *protocol.ConveyancePreference `json:"attestation_preference" validate:"omitempty,oneof=none indirect direct enterprise"`
ResidentKeyRequirement *protocol.ResidentKeyRequirement `json:"resident_key_requirement" validate:"omitempty,oneof=discouraged preferred required"`
}

func (dto *CreateWebauthnDto) ToModel(configModel models.Config) models.WebauthnConfig {
func (dto *CreateWebauthnConfigDto) toModel(configModel models.Config) models.WebauthnConfig {
webauthnConfigId, _ := uuid.NewV4()
now := time.Now()

webauthnConfig := models.WebauthnConfig{
ID: webauthnConfigId,
ConfigID: configModel.ID,
Timeout: dto.Timeout,
CreatedAt: now,
UpdatedAt: now,
UserVerification: dto.UserVerification,
ID: webauthnConfigId,
ConfigID: configModel.ID,
Timeout: dto.Timeout,
CreatedAt: now,
UpdatedAt: now,
}

if dto.AttestationPreference == nil {
webauthnConfig.AttestationPreference = protocol.PreferNoAttestation
} else {
webauthnConfig.AttestationPreference = *dto.AttestationPreference
}

return webauthnConfig
}

func (dto *CreateWebauthnConfigDto) ToPasskeyModel(configModel models.Config) models.WebauthnConfig {
passkeyConfig := dto.toModel(configModel)
passkeyConfig.Attachment = dto.Attachment

if dto.ResidentKeyRequirement == nil {
passkeyConfig.ResidentKeyRequirement = protocol.ResidentKeyRequirementRequired
} else {
passkeyConfig.ResidentKeyRequirement = *dto.ResidentKeyRequirement
}

if dto.UserVerification == nil {
passkeyConfig.UserVerification = protocol.VerificationRequired
} else {
passkeyConfig.UserVerification = *dto.UserVerification
}

return passkeyConfig
}

func (dto *CreateWebauthnConfigDto) ToMfaModel(configModel models.Config) models.WebauthnConfig {
mfaConfig := dto.toModel(configModel)
mfaConfig.IsMfa = true

if dto.AttestationPreference == nil {
mfaConfig.AttestationPreference = protocol.PreferNoAttestation
} else {
mfaConfig.AttestationPreference = *dto.AttestationPreference
}
shentschel marked this conversation as resolved.
Show resolved Hide resolved

if dto.ResidentKeyRequirement == nil {
mfaConfig.ResidentKeyRequirement = protocol.ResidentKeyRequirementDiscouraged
} else {
mfaConfig.ResidentKeyRequirement = *dto.ResidentKeyRequirement
}

if dto.UserVerification == nil {
mfaConfig.UserVerification = protocol.VerificationPreferred
} else {
mfaConfig.UserVerification = *dto.UserVerification
}

if dto.Attachment == nil {
cp := protocol.CrossPlatform
mfaConfig.Attachment = &cp
} else {
mfaConfig.Attachment = dto.Attachment
}

return mfaConfig
}
14 changes: 13 additions & 1 deletion server/api/dto/admin/response/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ import "github.com/teamhanko/passkey-server/persistence/models"
type GetConfigResponse struct {
Cors GetCorsResponse `json:"cors"`
Webauthn GetWebauthnResponse `json:"webauthn"`
Mfa GetWebauthnResponse `json:"mfa"`
}

func ToGetConfigResponse(config *models.Config) GetConfigResponse {
var passkeyConfig models.WebauthnConfig
var mfaConfig models.WebauthnConfig
for _, webauthnConfig := range config.WebauthnConfigs {
if webauthnConfig.IsMfa {
mfaConfig = webauthnConfig
} else {
passkeyConfig = webauthnConfig
}
}

return GetConfigResponse{
Cors: ToGetCorsResponse(&config.Cors),
Webauthn: ToGetWebauthnResponse(&config.WebauthnConfig),
Webauthn: ToGetWebauthnResponse(&passkeyConfig),
Mfa: ToGetWebauthnResponse(&mfaConfig),
}
}
18 changes: 12 additions & 6 deletions server/api/dto/admin/response/webatuhn.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import (
)

type GetWebauthnResponse struct {
RelyingParty GetRelyingPartyResponse `json:"relying_party"`
Timeout int `json:"timeout"`
UserVerification protocol.UserVerificationRequirement `json:"user_verification"`
RelyingParty GetRelyingPartyResponse `json:"relying_party"`
Timeout int `json:"timeout"`
UserVerification protocol.UserVerificationRequirement `json:"user_verification"`
Attachment *protocol.AuthenticatorAttachment `json:"attachment,omitempty"`
AttestationPreference protocol.ConveyancePreference `json:"attestation_preference"`
ResidentKeyRequirement protocol.ResidentKeyRequirement `json:"resident_key_requirement"`
}

func ToGetWebauthnResponse(webauthn *models.WebauthnConfig) GetWebauthnResponse {
return GetWebauthnResponse{
RelyingParty: ToGetRelyingPartyResponse(&webauthn.RelyingParty),
Timeout: webauthn.Timeout,
UserVerification: webauthn.UserVerification,
RelyingParty: ToGetRelyingPartyResponse(&webauthn.RelyingParty),
Timeout: webauthn.Timeout,
UserVerification: webauthn.UserVerification,
Attachment: webauthn.Attachment,
AttestationPreference: webauthn.AttestationPreference,
ResidentKeyRequirement: webauthn.ResidentKeyRequirement,
}
}
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,
}
}
10 changes: 9 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 | InitMfaLoginDto
}

type InitRegistrationDto struct {
Expand Down Expand Up @@ -91,3 +91,11 @@ func (initTransaction *InitTransactionDto) ToModel() (*models.Transaction, error
UpdatedAt: now,
}, nil
}

type InitLoginDto struct {
UserId *string `json:"user_id" validate:"omitempty,min=1"`
}

type InitMfaLoginDto struct {
UserId *string `json:"userId" validate:"required,min=1"`
shentschel marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion server/api/handler/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type credentialsHandler struct {
}

func NewCredentialsHandler(persister persistence.Persister) CredentialsHandler {
webauthnHandler := newWebAuthnHandler(persister)
webauthnHandler := newWebAuthnHandler(persister, false)

return &credentialsHandler{
webauthnHandler,
Expand Down
13 changes: 10 additions & 3 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 All @@ -19,7 +20,7 @@ type loginHandler struct {
}

func NewLoginHandler(persister persistence.Persister) WebauthnHandler {
webauthnHandler := newWebAuthnHandler(persister)
webauthnHandler := newWebAuthnHandler(persister, false)

return &loginHandler{
webauthnHandler,
Expand All @@ -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 @@ -41,7 +47,8 @@ func (lh *loginHandler) Init(ctx echo.Context) error {
service := services.NewLoginService(services.WebauthnServiceCreateParams{
Ctx: ctx,
Tenant: *h.Tenant,
WebauthnClient: *h.Webauthn,
WebauthnClient: *h.WebauthnClient,
UserId: dto.UserId,
UserPersister: userPersister,
SessionPersister: sessionPersister,
CredentialPersister: credentialPersister,
Expand Down Expand Up @@ -84,7 +91,7 @@ func (lh *loginHandler) Finish(ctx echo.Context) error {
service := services.NewLoginService(services.WebauthnServiceCreateParams{
Ctx: ctx,
Tenant: *h.Tenant,
WebauthnClient: *h.Webauthn,
WebauthnClient: *h.WebauthnClient,
UserPersister: userPersister,
SessionPersister: sessionPersister,
CredentialPersister: credentialPersister,
Expand Down
Loading
Loading