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

Chore: remove test persister #1876

Draft
wants to merge 45 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
14a3c80
feat: create otp_secrets table
lfleischmann Aug 26, 2024
79ed5a5
feat: create otp secret model
lfleischmann Aug 26, 2024
10cd3cc
feat: add mfa_only column to webauthn_credentials table
lfleischmann Aug 26, 2024
805fd8d
feat: add mfa only field to webauthn credential model
lfleischmann Aug 26, 2024
c4f35a8
feat: add mfa config (#1607)
bjoern-m Sep 2, 2024
77bbff9
feat: add otp secret persister (#1613)
bjoern-m Sep 2, 2024
3cf8ce3
feat: MFA usage sub flow (#1614)
bjoern-m Sep 3, 2024
69029d0
feat: include platform authenticator availybility in the preflight fl…
bjoern-m Sep 3, 2024
7cc3d15
feat: add mfa creation subflow
lfleischmann Sep 5, 2024
6f776fe
feat: adjust registration flow
lfleischmann Sep 5, 2024
48d7b19
feat: integrate mfa usage sub-flow
bjoern-m Sep 6, 2024
7cab7a1
feat: add pages for mfa (#1622)
bjoern-m Sep 9, 2024
e257953
feat: profile flow adjustments for mfa support
lfleischmann Sep 6, 2024
3acdde4
fix: suspension logic for mfa deletion actions
lfleischmann Sep 9, 2024
af1a9ce
feat: use dedicated action for security key creation options
lfleischmann Sep 9, 2024
0a243ea
fix: mfa method stash entry can be stale on profile flow
lfleischmann Sep 9, 2024
11cc6cd
feat: add new icons and english translations (#1626)
bjoern-m Sep 11, 2024
f82338d
fix: credential id encoding corrected (#1628)
bjoern-m Sep 11, 2024
38906cd
feat: add audit logs for mfa creation
lfleischmann Sep 10, 2024
aa3160e
feat: add a skip link to the mfa method chooser (#1630)
bjoern-m Sep 12, 2024
344e35d
feat: save the security key during login (#1629)
bjoern-m Sep 12, 2024
79b0797
feat: show security keys in profile
lfleischmann Sep 12, 2024
d2b8505
feat: add authenticator app management to profile (#1633)
bjoern-m Sep 18, 2024
8cdfd69
feat: prohibit security key first factor usage
lfleischmann Sep 12, 2024
3990670
feat: add all WA creds to exclude list on registration
lfleischmann Sep 17, 2024
a73d448
refactor: mfa stash entries and webauthn credential persistence
lfleischmann Sep 19, 2024
e82e079
refactor: simplify WA creation call
lfleischmann Sep 19, 2024
95a15cf
chore: adjust mfa flow
bjoern-m Sep 20, 2024
9a53e24
fix: mfa onboarding always shown during login
bjoern-m Sep 23, 2024
d1a0e79
fix: mfa onboarding not shown after password or email creation during…
bjoern-m Sep 23, 2024
f8ffc95
fix: mfa onboarding not shown without user detail onboarding
bjoern-m Sep 23, 2024
b9cff81
fix: correct skip/back behaviour
bjoern-m Sep 23, 2024
3bd1c52
feat: reuse generated otp secret when the code is invalid
bjoern-m Sep 26, 2024
a1898ec
chore: skip mfa prompt if the user only has a passkey
bjoern-m Sep 26, 2024
43be6f8
chore: adjust login flow
bjoern-m Sep 30, 2024
52d7354
chore: adjust recovery flow (#1655)
bjoern-m Oct 1, 2024
2ef57f5
feat: disable password, passcode endpoints when mfa enabled
lfleischmann Oct 9, 2024
c68b978
Feat: remember last used login method (#1674)
bjoern-m Oct 9, 2024
128f008
chore: remove omitempty from boolean (#1676)
FreddyDevelop Oct 10, 2024
0d189af
chore: improved error handling (#1679)
bjoern-m Oct 11, 2024
a986cf8
feat: update aaguid list (#1678)
FreddyDevelop Oct 14, 2024
9a815f3
fix: do not suspend webauthn action for MFA (#1778)
FreddyDevelop Oct 17, 2024
784e17f
fix: change texts (#1785)
FreddyDevelop Oct 17, 2024
818e6a3
Fix: UI issues (#1846)
bjoern-m Oct 22, 2024
ce7034e
chore: remove deprecated test persister
bjoern-m Oct 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type Config struct {
Emails Emails `yaml:"emails" json:"emails,omitempty" koanf:"emails" jsonschema:"title=emails"`
// `log` configures application logging.
Log LoggerConfig `yaml:"log" json:"log,omitempty" koanf:"log" jsonschema:"title=log"`
// `mfa` configures how multi-factor-authentication behaves.
MFA MFA `yaml:"mfa" json:"mfa,omitempty" koanf:"mfa" jsonschema:"title=mfa"`
// Deprecated. See child properties for suggested replacements.
Passcode Passcode `yaml:"passcode" json:"passcode,omitempty" koanf:"passcode" jsonschema:"title=passcode"`
// `passkey` configures how passkeys are acquired and used.
Expand Down
13 changes: 13 additions & 0 deletions backend/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ email_delivery:
port: "2500"
log:
log_health_and_metrics: true
mfa:
acquire_on_login: false
acquire_on_registration: true
enabled: true
optional: true
security_keys:
attestation_preference: direct
authenticator_attachment: cross-platform
enabled: true
limit: 10
user_verification: discouraged
totp:
enabled: true
passkey:
enabled: true
optional: true
Expand Down
20 changes: 20 additions & 0 deletions backend/config/config_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ func DefaultConfig() *Config {
RateLimiter: RateLimiter{
Enabled: true,
Store: RATE_LIMITER_STORE_IN_MEMORY,
OTPLimits: RateLimits{
Tokens: 3,
Interval: 1 * time.Minute,
},
PasswordLimits: RateLimits{
Tokens: 5,
Interval: 1 * time.Minute,
Expand Down Expand Up @@ -161,6 +165,22 @@ func DefaultConfig() *Config {
MinLength: 3,
MaxLength: 32,
},
MFA: MFA{
AcquireOnLogin: false,
AcquireOnRegistration: true,
Enabled: true,
Optional: true,
SecurityKeys: SecurityKeys{
AttestationPreference: "direct",
AuthenticatorAttachment: "cross-platform",
Enabled: true,
Limit: 10,
UserVerification: "discouraged",
},
TOTP: TOTP{
Enabled: true,
},
},
Debug: false,
}
}
36 changes: 36 additions & 0 deletions backend/config/config_mfa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package config

type SecurityKeys struct {
// `attestation_preference` is used to specify the preference regarding attestation conveyance during
// credential generation.
AttestationPreference string `yaml:"attestation_preference" json:"attestation_preference,omitempty" koanf:"attestation_preference" split_words:"true" jsonschema:"default=direct,enum=direct,enum=indirect,enum=none"`
// `authenticator_attachment` is used to specify the preference regarding authenticator attachment during credential registration.
AuthenticatorAttachment string `yaml:"authenticator_attachment" json:"authenticator_attachment,omitempty" koanf:"authenticator_attachment" split_words:"true" jsonschema:"default=cross-platform,enum=platform,enum=cross-platform,enum=no_preference"`
// `enabled` determines whether security keys are eligible for multi-factor-authentication.
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=true"`
// 'limit' determines the maximum number of security keys a user can register.
Limit int `yaml:"limit" json:"limit,omitempty" koanf:"limit" jsonschema:"default=10"`
// The setting applies to both WebAuthn registration and authentication ceremonies.
UserVerification string `yaml:"user_verification" json:"user_verification,omitempty" koanf:"user_verification" split_words:"true" jsonschema:"default=discouraged,enum=required,enum=preferred,enum=discouraged"`
}

type TOTP struct {
// `enabled` determines whether TOTP is eligible for multi-factor-authentication.
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=true"`
}

type MFA struct {
// `acquire_on_login` configures if users are prompted creating an MFA credential on login.
AcquireOnLogin bool `yaml:"acquire_on_login" json:"acquire_on_login" koanf:"acquire_on_login" jsonschema:"default=false"`
// `acquire_on_registration` configures if users are prompted creating an MFA credential on registration.
AcquireOnRegistration bool `yaml:"acquire_on_registration" json:"acquire_on_registration" koanf:"acquire_on_registration" jsonschema:"default=true"`
// `enabled` determines whether multi-factor-authentication is enabled.
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=true"`
// `optional` determines whether users must create an MFA credential when prompted. The MFA credential cannot be
// deleted if multi-factor-authentication is required (`optional: false`).
Optional bool `yaml:"optional" json:"optional" koanf:"optional" jsonschema:"default=true"`
// `security_keys` configures security key settings for multi-factor-authentication
SecurityKeys SecurityKeys `yaml:"security_keys" json:"security_keys,omitempty" koanf:"security_keys" jsonschema:"title=security_keys"`
// `totp` configures the TOTP (Time-Based One-Time-Password) method for multi-factor-authentication.
TOTP TOTP `yaml:"totp" json:"totp,omitempty" koanf:"totp" jsonschema:"title=totp"`
}
2 changes: 2 additions & 0 deletions backend/config/config_rate_limiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type RateLimiter struct {
Redis *RedisConfig `yaml:"redis_config" json:"redis_config,omitempty" koanf:"redis_config"`
// `passcode_limits` controls rate limits for passcode operations.
PasscodeLimits RateLimits `yaml:"passcode_limits" json:"passcode_limits,omitempty" koanf:"passcode_limits" split_words:"true"`
// `otp_limits` controls rate limits for OTP login attempts.
OTPLimits RateLimits `yaml:"otp_limits" json:"otp_limits,omitempty" koanf:"otp_limits" split_words:"true"`
// `password_limits` controls rate limits for password login operations.
PasswordLimits RateLimits `yaml:"password_limits" json:"password_limits,omitempty" koanf:"password_limits" split_words:"true"`
// `token_limits` controls rate limits for token exchange operations.
Expand Down
3 changes: 2 additions & 1 deletion backend/dto/intern/WebauthnCredential.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"time"
)

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

for _, name := range credential.Transport {
Expand Down
49 changes: 34 additions & 15 deletions backend/dto/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,37 @@ package dto

import (
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/persistence/models"
"time"
)

type MFAConfig struct {
AuthAppSetUp bool `json:"auth_app_set_up"`
TOTPEnabled bool `json:"totp_enabled"`
SecurityKeysEnabled bool `json:"security_keys_enabled"`
}

type ProfileData struct {
UserID uuid.UUID `json:"user_id"`
WebauthnCredentials []WebauthnCredentialResponse `json:"passkeys,omitempty"`
Emails []EmailResponse `json:"emails,omitempty"`
Username *Username `json:"username,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
UserID uuid.UUID `json:"user_id"`
Passkeys []WebauthnCredentialResponse `json:"passkeys,omitempty"`
SecurityKeys []WebauthnCredentialResponse `json:"security_keys,omitempty"`
MFAConfig MFAConfig `json:"mfa_config"`
Emails []EmailResponse `json:"emails,omitempty"`
Username *Username `json:"username,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func ProfileDataFromUserModel(user *models.User) *ProfileData {
var webauthnCredentials []WebauthnCredentialResponse
func ProfileDataFromUserModel(user *models.User, cfg *config.Config) *ProfileData {
var webauthnCredentials, securityKeys []WebauthnCredentialResponse
for _, webauthnCredentialModel := range user.WebauthnCredentials {
webauthnCredential := FromWebauthnCredentialModel(&webauthnCredentialModel)
webauthnCredentials = append(webauthnCredentials, *webauthnCredential)
if cfg.MFA.SecurityKeys.Enabled && webauthnCredentialModel.MFAOnly {
securityKeys = append(securityKeys, *webauthnCredential)
} else if cfg.Passkey.Enabled {
webauthnCredentials = append(webauthnCredentials, *webauthnCredential)
}
}

var emails []EmailResponse
Expand All @@ -29,11 +42,17 @@ func ProfileDataFromUserModel(user *models.User) *ProfileData {
}

return &ProfileData{
UserID: user.ID,
WebauthnCredentials: webauthnCredentials,
Emails: emails,
Username: FromUsernameModel(user.Username),
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
UserID: user.ID,
Passkeys: webauthnCredentials,
SecurityKeys: securityKeys,
MFAConfig: MFAConfig{
AuthAppSetUp: user.OTPSecret != nil,
TOTPEnabled: cfg.MFA.Enabled && cfg.MFA.TOTP.Enabled,
SecurityKeysEnabled: cfg.MFA.Enabled && cfg.MFA.SecurityKeys.Enabled,
},
Emails: emails,
Username: FromUsernameModel(user.Username),
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
17 changes: 16 additions & 1 deletion backend/flow_api/flow/capabilities/action_send_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ func (a RegisterClientCapabilities) GetDescription() string {

func (a RegisterClientCapabilities) Initialize(c flowpilot.InitializationContext) {
c.AddInputs(flowpilot.BooleanInput("webauthn_conditional_mediation_available").Hidden(true))
c.AddInputs(flowpilot.BooleanInput("webauthn_platform_authenticator_available").Hidden(true))
c.AddInputs(flowpilot.BooleanInput("webauthn_available").Required(true).Hidden(true))
}

func (a RegisterClientCapabilities) Execute(c flowpilot.ExecutionContext) error {
deps := a.GetDeps(c)

if valid := c.ValidateInputData(); !valid {
return c.Error(flowpilot.ErrorFormDataInvalid)
}

webauthnAvailable := c.Input().Get("webauthn_available").Bool()

err := c.Stash().Set(shared.StashPathWebauthnAvailable, webauthnAvailable)
if err != nil {
return err
Expand All @@ -40,5 +42,18 @@ func (a RegisterClientCapabilities) Execute(c flowpilot.ExecutionContext) error
return err
}

platformAuthenticatorAvailable := c.Input().Get("webauthn_platform_authenticator_available").Bool()
err = c.Stash().Set(shared.StashPathWebauthnPlatformAuthenticatorAvailable, platformAuthenticatorAvailable)
if err != nil {
return err
}

attachmentSupported := platformAuthenticatorAvailable ||
deps.Cfg.MFA.SecurityKeys.AuthenticatorAttachment != "platform"
err = c.Stash().Set(shared.StashPathSecurityKeyAttachmentSupported, attachmentSupported)
if err != nil {
return err
}

return c.Continue()
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,9 @@ func (a RegisterPassword) Execute(c flowpilot.ExecutionContext) error {

c.PreventRevert()

if err = c.ExecuteHook(shared.ScheduleMFACreationStates{}); err != nil {
return err
}

return c.Continue()
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,9 @@ func (a SkipCredentialOnboardingMethodChooser) Initialize(c flowpilot.Initializa
func (a SkipCredentialOnboardingMethodChooser) Execute(c flowpilot.ExecutionContext) error {
c.PreventRevert()

if err := c.ExecuteHook(shared.ScheduleMFACreationStates{}); err != nil {
return err
}

return c.Continue()
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ func (a SkipPasskey) Execute(c flowpilot.ExecutionContext) error {
return c.Continue(shared.StatePasswordCreation)
}

if err := c.ExecuteHook(shared.ScheduleMFACreationStates{}); err != nil {
return err
}

return c.Continue()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ func (a SkipPassword) Execute(c flowpilot.ExecutionContext) error {
return c.Continue(shared.StateOnboardingCreatePasskey)
}

if err := c.ExecuteHook(shared.ScheduleMFACreationStates{}); err != nil {
return err
}

return c.Continue()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (a WebauthnGenerateCreationOptions) Execute(c flowpilot.ExecutionContext) e
Username: &username,
}

sessionDataModel, creationOptions, err := deps.WebauthnService.GenerateCreationOptions(params)
sessionDataModel, creationOptions, err := deps.WebauthnService.GenerateCreationOptionsPasskey(params)
if err != nil {
return fmt.Errorf("failed to generate webauthn creation options: %w", err)
}
Expand All @@ -67,6 +67,11 @@ func (a WebauthnGenerateCreationOptions) Execute(c flowpilot.ExecutionContext) e
return err
}

err = c.Stash().Set(shared.StashPathCreateMFAOnlyCredential, false)
if err != nil {
return err
}

err = c.Payload().Set("creation_options", creationOptions)
if err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package credential_onboarding

import (
"errors"
"fmt"
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/flow_api/flow/shared"
"github.com/teamhanko/hanko/backend/flow_api/services"
"github.com/teamhanko/hanko/backend/flowpilot"
)

Expand All @@ -30,58 +26,19 @@ func (a WebauthnVerifyAttestationResponse) Initialize(c flowpilot.Initialization
}

func (a WebauthnVerifyAttestationResponse) Execute(c flowpilot.ExecutionContext) error {
deps := a.GetDeps(c)

if valid := c.ValidateInputData(); !valid {
return c.Error(flowpilot.ErrorFormDataInvalid)
}

if !c.Stash().Get(shared.StashPathWebauthnSessionDataID).Exists() {
return errors.New("webauthn_session_data_id does not exist in the stash")
}

sessionDataID, err := uuid.FromString(c.Stash().Get(shared.StashPathWebauthnSessionDataID).String())
if err != nil {
return fmt.Errorf("failed to parse webauthn_session_data_id: %w", err)
}

userID, err := uuid.FromString(c.Stash().Get(shared.StashPathUserID).String())
if err != nil {
return fmt.Errorf("failed to parse user_id into a uuid: %w", err)
}

username := c.Stash().Get(shared.StashPathUsername).String()
email := c.Stash().Get(shared.StashPathEmail).String()

params := services.VerifyAttestationResponseParams{
Tx: deps.Tx,
SessionDataID: sessionDataID,
PublicKey: c.Input().Get("public_key").String(),
UserID: userID,
Email: &email,
Username: &username,
if err := c.ExecuteHook(shared.VerifyAttestationResponse{}); err != nil {
return err
}

credential, err := deps.WebauthnService.VerifyAttestationResponse(params)
if err != nil {
if errors.Is(err, services.ErrInvalidWebauthnCredential) {
return c.Error(shared.ErrorPasskeyInvalid.Wrap(err))
}

return fmt.Errorf("failed to verify attestation response: %w", err)
}

err = c.Stash().Set(shared.StashPathWebauthnCredential, credential)
if err != nil {
return fmt.Errorf("failed to set webauthn_credential to the stash: %w", err)
}
c.PreventRevert()

err = c.Stash().Set(shared.StashPathUserHasWebauthnCredential, true)
if err != nil {
return fmt.Errorf("failed to set user_has_webauthn_credential to the stash: %w", err)
if err := c.ExecuteHook(shared.ScheduleMFACreationStates{}); err != nil {
return err
}

c.PreventRevert()

return c.Continue()
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func (a ContinueToPasscodeConfirmationRecovery) Initialize(c flowpilot.Initializ
}

func (a ContinueToPasscodeConfirmationRecovery) Execute(c flowpilot.ExecutionContext) error {
if err := c.Stash().Set(shared.StashPathLoginMethod, "password"); err != nil {
return fmt.Errorf("failed to set login_method to stash: %w", err)
}

if len(c.Stash().Get(shared.StashPathUserID).String()) > 0 {
if err := c.Stash().Set(shared.StashPathPasscodeTemplate, "recovery"); err != nil {
return fmt.Errorf("failed to set passcode_template to the stash: %w", err)
Expand All @@ -37,5 +41,7 @@ func (a ContinueToPasscodeConfirmationRecovery) Execute(c flowpilot.ExecutionCon
}
}

return c.Continue(shared.StatePasscodeConfirmation, shared.StateLoginPasswordRecovery)
_ = c.Stash().Set(shared.StashPathPasswordRecoveryPending, true)

return c.Continue(shared.StatePasscodeConfirmation)
}
Loading
Loading