Skip to content

Commit

Permalink
feat(mfa): rework mfa persistence
Browse files Browse the repository at this point in the history
* mfa config has now its own database table
* mfa config does not require RP config anymore
* webauthn client for mfa uses rp from passkey config
* update openapi specs to reflect changes

Closes: #45
  • Loading branch information
Stefan Jacobi committed Mar 25, 2024
1 parent 6f8b064 commit ab596c5
Show file tree
Hide file tree
Showing 36 changed files with 545 additions and 188 deletions.
2 changes: 1 addition & 1 deletion server/api/dto/admin/request/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
type CreateConfigDto struct {
Cors CreateCorsDto `json:"cors" validate:"required"`
Webauthn CreateWebauthnConfigDto `json:"webauthn" validate:"required"`
Mfa CreateWebauthnConfigDto `json:"mfa" validate:"required"`
Mfa CreateMFAConfigDto `json:"mfa" validate:"required"`
}

func (dto *CreateConfigDto) ToModel(tenant models.Tenant) models.Config {
Expand Down
55 changes: 55 additions & 0 deletions server/api/dto/admin/request/mfa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package request

import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/gofrs/uuid"
"github.com/teamhanko/passkey-server/persistence/models"
"time"
)

type CreateMFAConfigDto struct {
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 *CreateMFAConfigDto) ToModel(configModel models.Config) models.MfaConfig {
mfaConfigId, _ := uuid.NewV4()
now := time.Now()

mfaConfig := models.MfaConfig{
ID: mfaConfigId,
ConfigID: configModel.ID,
Timeout: dto.Timeout,
CreatedAt: now,
UpdatedAt: now,
}

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

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 {
mfaConfig.Attachment = protocol.CrossPlatform
} else {
mfaConfig.Attachment = *dto.Attachment
}

return mfaConfig
}
49 changes: 6 additions & 43 deletions server/api/dto/admin/request/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,24 @@ type CreateWebauthnConfigDto struct {
ResidentKeyRequirement *protocol.ResidentKeyRequirement `json:"resident_key_requirement" validate:"omitempty,oneof=discouraged preferred required"`
}

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

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

if dto.AttestationPreference == nil {
webauthnConfig.AttestationPreference = protocol.PreferNoAttestation
passkeyConfig.AttestationPreference = protocol.PreferNoAttestation
} else {
webauthnConfig.AttestationPreference = *dto.AttestationPreference
passkeyConfig.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 {
Expand All @@ -55,35 +50,3 @@ func (dto *CreateWebauthnConfigDto) ToPasskeyModel(configModel models.Config) mo

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
}

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
}
16 changes: 3 additions & 13 deletions server/api/dto/admin/response/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,13 @@ import "github.com/teamhanko/passkey-server/persistence/models"
type GetConfigResponse struct {
Cors GetCorsResponse `json:"cors"`
Webauthn GetWebauthnResponse `json:"webauthn"`
Mfa GetWebauthnResponse `json:"mfa"`
MFA GetMFAResponse `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(&passkeyConfig),
Mfa: ToGetWebauthnResponse(&mfaConfig),
Webauthn: ToGetWebauthnResponse(&config.WebauthnConfig),
MFA: ToGetMFAResponse(config.MfaConfig),
}
}
24 changes: 24 additions & 0 deletions server/api/dto/admin/response/mfa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package response

import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/teamhanko/passkey-server/persistence/models"
)

type GetMFAResponse struct {
Timeout int `json:"timeout"`
UserVerification protocol.UserVerificationRequirement `json:"user_verification"`
Attachment protocol.AuthenticatorAttachment `json:"attachment"`
AttestationPreference protocol.ConveyancePreference `json:"attestation_preference"`
ResidentKeyRequirement protocol.ResidentKeyRequirement `json:"resident_key_requirement"`
}

func ToGetMFAResponse(webauthn *models.MfaConfig) GetMFAResponse {
return GetMFAResponse{
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_credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"time"
)

func WebauthnCredentialToModel(credential *webauthn.Credential, userId string, webauthnUserId uuid.UUID, backupEligible bool, backupState bool, authenticatorMetadata mapper.AuthenticatorMetadata) *models.WebauthnCredential {
func WebauthnCredentialToModel(credential *webauthn.Credential, userId string, webauthnUserId uuid.UUID, backupEligible bool, backupState bool, authenticatorMetadata mapper.AuthenticatorMetadata, isMFACredential bool) *models.WebauthnCredential {
now := time.Now().UTC()
aaguid, _ := uuid.FromBytes(credential.Authenticator.AAGUID)
credentialID := base64.RawURLEncoding.EncodeToString(credential.ID)
Expand All @@ -34,6 +34,7 @@ func WebauthnCredentialToModel(credential *webauthn.Credential, userId string, w
UpdatedAt: now,
BackupEligible: backupEligible,
BackupState: backupState,
IsMFA: isMFACredential,

WebauthnUserID: webauthnUserId,
}
Expand Down
2 changes: 1 addition & 1 deletion server/api/dto/request/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,5 @@ type InitLoginDto struct {
}

type InitMfaLoginDto struct {
UserId *string `json:"userId" validate:"required,min=1"`
UserId *string `json:"user_id" validate:"required,min=1"`
}
2 changes: 2 additions & 0 deletions server/api/handler/admin/tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func (th *TenantHandler) Create(ctx echo.Context) error {
AuditConfigPersister: th.persister.GetAuditLogConfigPersister(tx),
SecretPersister: th.persister.GetSecretsPersister(tx),
JwkPersister: th.persister.GetJwkPersister(tx),
MFAConfigPersister: th.persister.GetMFAConfigPersister(tx),
})

createResponse, err := service.Create(dto)
Expand Down Expand Up @@ -166,6 +167,7 @@ func (th *TenantHandler) UpdateConfig(ctx echo.Context) error {
RelyingPartyPerister: th.persister.GetWebauthnRelyingPartyPersister(tx),
AuditConfigPersister: th.persister.GetAuditLogConfigPersister(tx),
SecretPersister: th.persister.GetSecretsPersister(tx),
MFAConfigPersister: th.persister.GetMFAConfigPersister(tx),
})

err := service.UpdateConfig(dto)
Expand Down
10 changes: 8 additions & 2 deletions server/api/handler/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/teamhanko/passkey-server/persistence"
"github.com/teamhanko/passkey-server/persistence/models"
"net/http"
"strings"
)

type loginHandler struct {
Expand All @@ -39,6 +40,11 @@ func (lh *loginHandler) Init(ctx echo.Context) error {
return err
}

apiKey := ctx.Request().Header.Get("apiKey")
if dto.UserId != nil && strings.TrimSpace(apiKey) == "" {
return echo.NewHTTPError(http.StatusBadRequest, "api key is missing")
}

return lh.persister.GetConnection().Transaction(func(tx *pop.Connection) error {
userPersister := lh.persister.GetWebauthnUserPersister(tx)
sessionPersister := lh.persister.GetWebauthnSessionDataPersister(tx)
Expand All @@ -55,12 +61,12 @@ func (lh *loginHandler) Init(ctx echo.Context) error {
})

credentialAssertion, err := service.Initialize()
err = lh.handleError(h.AuditLog, models.AuditLogWebAuthnAuthenticationInitFailed, tx, ctx, nil, nil, err)
err = lh.handleError(h.AuditLog, models.AuditLogWebAuthnAuthenticationInitFailed, tx, ctx, dto.UserId, nil, err)
if err != nil {
return err
}

auditErr := h.AuditLog.CreateWithConnection(tx, models.AuditLogWebAuthnAuthenticationInitSucceeded, nil, nil, nil)
auditErr := h.AuditLog.CreateWithConnection(tx, models.AuditLogWebAuthnAuthenticationInitSucceeded, dto.UserId, nil, nil)
if auditErr != nil {
ctx.Logger().Error(auditErr)
return fmt.Errorf(auditlog.CreationFailureFormat, auditErr)
Expand Down
6 changes: 4 additions & 2 deletions server/api/handler/mfa_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,16 @@ func (lh *mfaLoginHandler) Init(ctx echo.Context) error {
UserPersister: userPersister,
SessionPersister: sessionPersister,
CredentialPersister: credentialPersister,
UseMFA: true,
})

credentialAssertion, err := service.Initialize()
err = lh.handleError(h.AuditLog, models.AuditLogWebAuthnAuthenticationInitFailed, tx, ctx, nil, nil, err)
err = lh.handleError(h.AuditLog, models.AuditLogWebAuthnAuthenticationInitFailed, tx, ctx, dto.UserId, nil, err)
if err != nil {
return err
}

auditErr := h.AuditLog.CreateWithConnection(tx, models.AuditLogWebAuthnAuthenticationInitSucceeded, nil, nil, nil)
auditErr := h.AuditLog.CreateWithConnection(tx, models.AuditLogWebAuthnAuthenticationInitSucceeded, dto.UserId, nil, nil)
if auditErr != nil {
ctx.Logger().Error(auditErr)
return fmt.Errorf(auditlog.CreationFailureFormat, auditErr)
Expand Down Expand Up @@ -96,6 +97,7 @@ func (lh *mfaLoginHandler) Finish(ctx echo.Context) error {
SessionPersister: sessionPersister,
CredentialPersister: credentialPersister,
Generator: h.Generator,
UseMFA: true,
})

token, userId, err := service.Finalize(parsedRequest)
Expand Down
6 changes: 4 additions & 2 deletions server/api/handler/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (r *registrationHandler) Init(ctx echo.Context) error {

var h *helper.WebauthnContext
var hErr error
if r.UseMfaClient {
if r.UseMFAClient {
h, hErr = helper.GetMfaHandlerContext(ctx)
} else {
h, hErr = helper.GetHandlerContext(ctx)
Expand All @@ -61,6 +61,7 @@ func (r *registrationHandler) Init(ctx echo.Context) error {
UserPersister: userPersister,
SessionPersister: sessionPersister,
CredentialPersister: credentialPersister,
UseMFA: r.UseMFAClient,
})

credentialCreation, userId, err := service.Initialize(webauthnUser)
Expand Down Expand Up @@ -88,7 +89,7 @@ func (r *registrationHandler) Finish(ctx echo.Context) error {

var h *helper.WebauthnContext
var hErr error
if r.UseMfaClient {
if r.UseMFAClient {
h, hErr = helper.GetMfaHandlerContext(ctx)
} else {
h, hErr = helper.GetHandlerContext(ctx)
Expand All @@ -113,6 +114,7 @@ func (r *registrationHandler) Finish(ctx echo.Context) error {
CredentialPersister: credentialPersister,
Generator: h.Generator,
AuthenticatorMetadata: r.AuthenticatorMetadata,
UseMFA: r.UseMFAClient,
})

token, userId, err := service.Finalize(parsedRequest)
Expand Down
6 changes: 3 additions & 3 deletions server/api/handler/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ type WebauthnHandler interface {

type webauthnHandler struct {
persister persistence.Persister
UseMfaClient bool
UseMFAClient bool
}

func newWebAuthnHandler(persister persistence.Persister, useMfaClient bool) *webauthnHandler {
func newWebAuthnHandler(persister persistence.Persister, useMFAClient bool) *webauthnHandler {
return &webauthnHandler{
persister: persister,
UseMfaClient: useMfaClient,
UseMFAClient: useMFAClient,
}
}

Expand Down
8 changes: 4 additions & 4 deletions server/api/helper/context_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type WebauthnContext struct {
Generator jwt.Generator
}

func getContext(ctx echo.Context, webauthnClientKey string, jwtGeneratorKey string) (*WebauthnContext, error) {
func getContext(ctx echo.Context, webauthnClientKey string) (*WebauthnContext, error) {
ctxTenant := ctx.Get("tenant")
if ctxTenant == nil {
return nil, echo.NewHTTPError(http.StatusNotFound, "Unable to find tenant")
Expand All @@ -30,7 +30,7 @@ func getContext(ctx echo.Context, webauthnClientKey string, jwtGeneratorKey stri
webauthnClient = webauthnClientCtx.(*webauthn.WebAuthn)
}

jwtGeneratorCtx := ctx.Get(jwtGeneratorKey)
jwtGeneratorCtx := ctx.Get("jwt_generator")
var jwtGenerator jwt.Generator
if jwtGeneratorCtx != nil {
jwtGenerator = jwtGeneratorCtx.(jwt.Generator)
Expand All @@ -52,9 +52,9 @@ func getContext(ctx echo.Context, webauthnClientKey string, jwtGeneratorKey stri
}

func GetHandlerContext(ctx echo.Context) (*WebauthnContext, error) {
return getContext(ctx, "webauthn_client", "jwt_generator")
return getContext(ctx, "webauthn_client")
}

func GetMfaHandlerContext(ctx echo.Context) (*WebauthnContext, error) {
return getContext(ctx, "mfa_client", "mfa_jwt_generator")
return getContext(ctx, "mfa_client")
}
Loading

0 comments on commit ab596c5

Please sign in to comment.