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: introduce mfa #1645

Merged
merged 50 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
50 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
4fffb95
Chore: remove test persister (#1876)
bjoern-m Oct 25, 2024
c5549b2
Update backend/flow_api/services/webauthn.go
bjoern-m Oct 30, 2024
b341e53
Update backend/dto/profile.go
bjoern-m Oct 30, 2024
dd7b0c3
fix: otp validation uses the rate limiter key for passwords
bjoern-m Oct 30, 2024
71f97fc
chore: add otp-limits to the default config
bjoern-m Oct 30, 2024
603a5b5
chore: add explanation for 'UserVerification' setting on security keys
bjoern-m Oct 30, 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.
bjoern-m marked this conversation as resolved.
Show resolved Hide resolved
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
60 changes: 19 additions & 41 deletions backend/crypto/jwk/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,50 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/test"
"testing"
)

type mockJwkPersister struct {
jwks []models.Jwk
func TestJWKManagerSuite(t *testing.T) {
s := new(jwkManagerSuite)
suite.Run(t, s)
}

func (m *mockJwkPersister) Get(i int) (*models.Jwk, error) {
for _, v := range m.jwks {
if v.ID == i {
return &v, nil
}
}
return nil, nil
type jwkManagerSuite struct {
test.Suite
}

func (m *mockJwkPersister) GetAll() ([]models.Jwk, error) {
return m.jwks, nil
}

func (m *mockJwkPersister) GetLast() (*models.Jwk, error) {
index := len(m.jwks)
return &m.jwks[index-1], nil
}

func (m *mockJwkPersister) Create(jwk models.Jwk) error {
//increment id
index := len(m.jwks)
jwk.ID = index

m.jwks = append(m.jwks, jwk)
return nil
}

func TestDefaultManager(t *testing.T) {
func (s *jwkManagerSuite) TestDefaultManager() {
keys := []string{"asfnoadnfoaegnq3094intoaegjnoadjgnoadng", "apdisfoaiegnoaiegnbouaebgn982"}
//persister := mockJwkPersister{jwks: []models.Jwk{}}
persister := test.NewJwkPersister(nil)

persister := s.Storage.GetJwkPersister()

dm, err := NewDefaultManager(keys, persister)
require.NoError(t, err)
require.NoError(s.T(), err)
all, err := persister.GetAll()

require.NoError(t, err)
assert.Equal(t, 2, len(all))
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, len(all))

js, err := dm.GetPublicKeys()
require.NoError(t, err)
assert.Equal(t, 2, js.Len())
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, js.Len())

sk, err := dm.GetSigningKey()
require.NoError(t, err)
require.NoError(s.T(), err)

token := jwt.New()
token.Set("Payload", "isJustFine")
signed, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, sk))
require.NoError(t, err)
require.NoError(s.T(), err)

// Get Public Key of signing key
pk, err := sk.PublicKey()
require.NoError(t, err)
require.NoError(s.T(), err)

// Parse and Verify
tokenParsed, err := jwt.Parse(signed, jwt.WithKey(jwa.RS256, pk))
assert.NoError(t, err)
assert.Equal(t, token, tokenParsed)
assert.NoError(s.T(), err)
assert.Equal(s.T(), token, tokenParsed)
}
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)
}
bjoern-m marked this conversation as resolved.
Show resolved Hide resolved
}

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,
}
}
Loading
Loading