Flow api fix config usage (#1518)

* fix: check email limit in profile

* fix: check email length in profile

* fix: check passkey limit in profile

* fix: set passcode_template when adding an email at login

* feat: increase flow TTL

* feat: add debug flag to config

* feat: add password & token exchange rate limiting

* fix: fix password & exchange token rate limiting

* fix: check if email already exists

check if the email address already exists before storing it in the DB when the user should/must add an email address in a login flow.
This commit is contained in:
Frederic Jahn
2024-07-18 15:51:43 +02:00
committed by GitHub
parent 40b5964ccc
commit f720dac927
16 changed files with 141 additions and 72 deletions

View File

@@ -45,6 +45,7 @@ type Config struct {
Webhooks WebhookSettings `yaml:"webhooks" json:"webhooks,omitempty" koanf:"webhooks"`
Email Email `yaml:"email" json:"email,omitempty" koanf:"email"`
Username Username `yaml:"username" json:"username,omitempty" koanf:"username"`
Debug bool `yaml:"debug" json:"debug,omitempty" koanf:"debug"`
}
var (

View File

@@ -164,5 +164,6 @@ func DefaultConfig() *Config {
MinLength: 3,
MaxLength: 40,
},
Debug: false,
}
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/teamhanko/hanko/backend/flow_api/services"
"github.com/teamhanko/hanko/backend/flowpilot"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/rate_limiter"
)
type PasswordLogin struct {
@@ -40,6 +41,22 @@ func (a PasswordLogin) Execute(c flowpilot.ExecutionContext) error {
return c.Error(flowpilot.ErrorFormDataInvalid)
}
if deps.Cfg.RateLimiter.Enabled {
rateLimitKey := rate_limiter.CreateRateLimitPasswordKey(deps.HttpContext.RealIP(), c.Stash().Get(shared.StashPathUserIdentification).String())
retryAfterSeconds, ok, err := rate_limiter.Limit2(deps.PasswordRateLimiter, rateLimitKey)
if err != nil {
return fmt.Errorf("rate limiter failed: %w", err)
}
if !ok {
err = c.Payload().Set("retry_after", retryAfterSeconds)
if err != nil {
return fmt.Errorf("failed to set a value for retry_after to the payload: %w", err)
}
return c.Error(shared.ErrorRateLimitExceeded.Wrap(fmt.Errorf("rate limit exceeded for: %s", rateLimitKey)))
}
}
var userID uuid.UUID
if c.Stash().Get(shared.StashPathEmail).Exists() {
@@ -69,14 +86,6 @@ func (a PasswordLogin) Execute(c flowpilot.ExecutionContext) error {
return a.wrongCredentialsError(c)
}
// TODO
//if h.rateLimiter != nil {
// err := rate_limiter.Limit(h.rateLimiter, userId, c)
// if err != nil {
// return err
// }
//}
err := deps.PasswordService.VerifyPassword(userID, c.Input().Get("password").String())
if err != nil {
if errors.Is(err, services.ErrorPasswordInvalid) {

View File

@@ -35,8 +35,8 @@ func (a ReSendPasscode) Execute(c flowpilot.ExecutionContext) error {
}
if deps.Cfg.RateLimiter.Enabled {
rateLimitKey := rate_limiter.CreateRateLimitKey(deps.HttpContext.RealIP(), c.Stash().Get(shared.StashPathEmail).String())
resendAfterSeconds, ok, err := rate_limiter.Limit2(deps.RateLimiter, rateLimitKey)
rateLimitKey := rate_limiter.CreateRateLimitPasscodeKey(deps.HttpContext.RealIP(), c.Stash().Get(shared.StashPathEmail).String())
resendAfterSeconds, ok, err := rate_limiter.Limit2(deps.PasscodeRateLimiter, rateLimitKey)
if err != nil {
return fmt.Errorf("rate limiter failed: %w", err)
}

View File

@@ -30,8 +30,8 @@ func (h SendPasscode) Execute(c flowpilot.HookExecutionContext) error {
}
if deps.Cfg.RateLimiter.Enabled {
rateLimitKey := rate_limiter.CreateRateLimitKey(deps.HttpContext.RealIP(), c.Stash().Get(shared.StashPathEmail).String())
resendAfterSeconds, ok, err := rate_limiter.Limit2(deps.RateLimiter, rateLimitKey)
rateLimitKey := rate_limiter.CreateRateLimitPasscodeKey(deps.HttpContext.RealIP(), c.Stash().Get(shared.StashPathEmail).String())
resendAfterSeconds, ok, err := rate_limiter.Limit2(deps.PasscodeRateLimiter, rateLimitKey)
if err != nil {
return fmt.Errorf("rate limiter failed: %w", err)
}

View File

@@ -97,8 +97,7 @@ var LoginFlow = flowpilot.NewFlow(shared.FlowLogin).
CredentialUsageSubFlow,
CredentialOnboardingSubFlow,
UserDetailsSubFlow).
TTL(10 * time.Minute).
Debug(true)
TTL(24 * time.Hour)
var RegistrationFlow = flowpilot.NewFlow(shared.FlowRegistration).
State(shared.StateRegistrationInit,
@@ -119,8 +118,7 @@ var RegistrationFlow = flowpilot.NewFlow(shared.FlowRegistration).
CredentialUsageSubFlow,
CredentialOnboardingSubFlow,
UserDetailsSubFlow).
TTL(10 * time.Minute).
Debug(true)
TTL(24 * time.Hour)
var ProfileFlow = flowpilot.NewFlow(shared.FlowProfile).
State(shared.StateProfileInit,
@@ -151,5 +149,4 @@ var ProfileFlow = flowpilot.NewFlow(shared.FlowProfile).
SubFlows(
CapabilitiesSubFlow,
CredentialUsageSubFlow).
TTL(10 * time.Minute).
Debug(true)
TTL(24 * time.Hour)

View File

@@ -76,6 +76,10 @@ func (a ContinueWithLoginIdentifier) Execute(c flowpilot.ExecutionContext) error
identifierInputName, identifierInputValue, treatIdentifierAsEmail := a.analyzeIdentifierInputs(c)
if err := c.Stash().Set(shared.StashPathUserIdentification, identifierInputValue); err != nil {
return fmt.Errorf("failed to set user_identification to stash: %w", err)
}
if len(identifierInputValue) == 0 {
return c.Error(flowpilot.ErrorFormDataInvalid)
}

View File

@@ -23,10 +23,12 @@ func (a EmailCreate) GetDescription() string {
func (a EmailCreate) Initialize(c flowpilot.InitializationContext) {
deps := a.GetDeps(c)
if !deps.Cfg.Email.Enabled {
userModel, ok := c.Get("session_user").(*models.User)
if !deps.Cfg.Email.Enabled || (ok && len(userModel.Emails) >= deps.Cfg.Email.Limit) {
c.SuspendAction()
} else {
c.AddInputs(flowpilot.EmailInput("email").Required(true).TrimSpace(true).LowerCase(true))
c.AddInputs(flowpilot.EmailInput("email").Required(true).MaxLength(deps.Cfg.Email.MaxLength).TrimSpace(true).LowerCase(true))
}
}

View File

@@ -24,7 +24,9 @@ func (a WebauthnCredentialCreate) GetDescription() string {
func (a WebauthnCredentialCreate) Initialize(c flowpilot.InitializationContext) {
deps := a.GetDeps(c)
if !deps.Cfg.Passkey.Enabled || !c.Stash().Get(shared.StashPathWebauthnAvailable).Bool() {
userModel, ok := c.Get("session_user").(*models.User)
if !deps.Cfg.Passkey.Enabled || !c.Stash().Get(shared.StashPathWebauthnAvailable).Bool() || (ok && len(userModel.WebauthnCredentials) >= deps.Cfg.Passkey.Limit) {
c.SuspendAction()
}
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/teamhanko/hanko/backend/flowpilot"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/rate_limiter"
"time"
)
@@ -31,6 +32,22 @@ func (a ExchangeToken) Execute(c flowpilot.ExecutionContext) error {
deps := a.GetDeps(c)
if deps.Cfg.RateLimiter.Enabled {
rateLimitKey := rate_limiter.CreateRateLimitTokenExchangeKey(deps.HttpContext.RealIP())
retryAfterSeconds, ok, err := rate_limiter.Limit2(deps.TokenExchangeRateLimiter, rateLimitKey)
if err != nil {
return fmt.Errorf("rate limiter failed: %w", err)
}
if !ok {
err = c.Payload().Set("retry_after", retryAfterSeconds)
if err != nil {
return fmt.Errorf("failed to set a value for retry_after to the payload: %w", err)
}
return c.Error(ErrorRateLimitExceeded.Wrap(fmt.Errorf("rate limit exceeded for: %s", rateLimitKey)))
}
}
tokenModel, err := deps.Persister.GetTokenPersisterWithConnection(deps.Tx).GetByValue(c.Input().Get("token").String())
if err != nil {
return fmt.Errorf("failed to fetch token from db: %w", err)

View File

@@ -19,4 +19,5 @@ const (
StashPathWebauthnConditionalMediationAvailable string = "webauthn_conditional_mediation_available"
StashPathWebauthnCredential string = "webauthn_credential"
StashPathWebauthnSessionDataID string = "webauthn_session_data_id"
StashPathUserIdentification string = "user_identification"
)

View File

@@ -15,18 +15,20 @@ import (
)
type Dependencies struct {
Cfg config.Config
HttpContext echo.Context
PasscodeService services.Passcode
PasswordService services.Password
WebauthnService services.WebauthnService
SamlService saml.Service
Persister persistence.Persister
SessionManager session.Manager
RateLimiter limiter.Store
Tx *pop.Connection
AuthenticatorMetadata mapper.AuthenticatorMetadata
AuditLogger auditlog.Logger
Cfg config.Config
HttpContext echo.Context
PasscodeService services.Passcode
PasswordService services.Password
WebauthnService services.WebauthnService
SamlService saml.Service
Persister persistence.Persister
SessionManager session.Manager
PasscodeRateLimiter limiter.Store
PasswordRateLimiter limiter.Store
TokenExchangeRateLimiter limiter.Store
Tx *pop.Connection
AuthenticatorMetadata mapper.AuthenticatorMetadata
AuditLogger auditlog.Logger
}
type Action struct{}

View File

@@ -49,6 +49,16 @@ func (a EmailAddressSet) Execute(c flowpilot.ExecutionContext) error {
email := strings.TrimSpace(c.Input().Get("email").String())
emailModel := models.NewEmail(&userID, email)
existingEmail, err := deps.Persister.GetEmailPersister().FindByAddress(email)
if err != nil {
return fmt.Errorf("failed to get email from db: %w", err)
}
if existingEmail != nil {
c.Input().SetError("email", shared.ErrorEmailAlreadyExists)
return c.Error(flowpilot.ErrorFormDataInvalid)
}
err = deps.Persister.GetEmailPersisterWithConnection(deps.Tx).Create(*emailModel)
if err != nil {
return fmt.Errorf("failed to create a new email: %w", err)
@@ -65,6 +75,11 @@ func (a EmailAddressSet) Execute(c flowpilot.ExecutionContext) error {
return fmt.Errorf("failed to set email to the stash: %w", err)
}
err = c.Stash().Set(shared.StashPathPasscodeTemplate, "email_verification")
if err != nil {
return fmt.Errorf("failed to set passcode_template to the stash: %w", err)
}
if deps.Cfg.Email.RequireVerification {
return c.Continue(shared.StatePasscodeConfirmation)
}

View File

@@ -23,28 +23,30 @@ import (
)
type FlowPilotHandler struct {
Persister persistence.Persister
Cfg config.Config
PasscodeService services.Passcode
PasswordService services.Password
WebauthnService services.WebauthnService
SamlService saml.Service
SessionManager session.Manager
RateLimiter limiter.Store
AuthenticatorMetadata mapper.AuthenticatorMetadata
AuditLogger auditlog.Logger
Persister persistence.Persister
Cfg config.Config
PasscodeService services.Passcode
PasswordService services.Password
WebauthnService services.WebauthnService
SamlService saml.Service
SessionManager session.Manager
PasscodeRateLimiter limiter.Store
PasswordRateLimiter limiter.Store
TokenExchangeRateLimiter limiter.Store
AuthenticatorMetadata mapper.AuthenticatorMetadata
AuditLogger auditlog.Logger
}
func (h *FlowPilotHandler) RegistrationFlowHandler(c echo.Context) error {
return h.executeFlow(c, flow.RegistrationFlow.MustBuild())
return h.executeFlow(c, flow.RegistrationFlow.Debug(h.Cfg.Debug).MustBuild())
}
func (h *FlowPilotHandler) LoginFlowHandler(c echo.Context) error {
return h.executeFlow(c, flow.LoginFlow.MustBuild())
return h.executeFlow(c, flow.LoginFlow.Debug(h.Cfg.Debug).MustBuild())
}
func (h *FlowPilotHandler) ProfileFlowHandler(c echo.Context) error {
return h.executeFlow(c, flow.ProfileFlow.MustBuild())
return h.executeFlow(c, flow.ProfileFlow.Debug(h.Cfg.Debug).MustBuild())
}
func (h *FlowPilotHandler) executeFlow(c echo.Context, flow flowpilot.Flow) error {
@@ -56,18 +58,20 @@ func (h *FlowPilotHandler) executeFlow(c echo.Context, flow flowpilot.Flow) erro
txFunc := func(tx *pop.Connection) error {
deps := &shared.Dependencies{
Cfg: h.Cfg,
RateLimiter: h.RateLimiter,
Tx: tx,
Persister: h.Persister,
HttpContext: c,
SessionManager: h.SessionManager,
PasscodeService: h.PasscodeService,
PasswordService: h.PasswordService,
WebauthnService: h.WebauthnService,
SamlService: h.SamlService,
AuthenticatorMetadata: h.AuthenticatorMetadata,
AuditLogger: h.AuditLogger,
Cfg: h.Cfg,
PasscodeRateLimiter: h.PasscodeRateLimiter,
PasswordRateLimiter: h.PasswordRateLimiter,
TokenExchangeRateLimiter: h.TokenExchangeRateLimiter,
Tx: tx,
Persister: h.Persister,
HttpContext: c,
SessionManager: h.SessionManager,
PasscodeService: h.PasscodeService,
PasswordService: h.PasswordService,
WebauthnService: h.WebauthnService,
SamlService: h.SamlService,
AuthenticatorMetadata: h.AuthenticatorMetadata,
AuditLogger: h.AuditLogger,
}
flow.Set("deps", deps)

View File

@@ -41,9 +41,13 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
panic(fmt.Errorf("failed to create session generator: %w", err))
}
var rateLimiter limiter.Store
var passcodeRateLimiter limiter.Store
var passwordRateLimiter limiter.Store
var tokenExchangeRateLimiter limiter.Store
if cfg.RateLimiter.Enabled {
rateLimiter = rate_limiter.NewRateLimiter(cfg.RateLimiter, cfg.RateLimiter.PasscodeLimits)
passcodeRateLimiter = rate_limiter.NewRateLimiter(cfg.RateLimiter, cfg.RateLimiter.PasscodeLimits)
passwordRateLimiter = rate_limiter.NewRateLimiter(cfg.RateLimiter, cfg.RateLimiter.PasswordLimits)
tokenExchangeRateLimiter = rate_limiter.NewRateLimiter(cfg.RateLimiter, cfg.RateLimiter.TokenLimits)
}
auditLogger := auditlog.NewLogger(persister, cfg.AuditLog)
@@ -51,16 +55,18 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
samlService := saml.NewSamlService(cfg, persister)
flowAPIHandler := flow_api.FlowPilotHandler{
Persister: persister,
Cfg: *cfg,
PasscodeService: passcodeService,
PasswordService: passwordService,
WebauthnService: webauthnService,
SessionManager: sessionManager,
RateLimiter: rateLimiter,
AuthenticatorMetadata: authenticatorMetadata,
AuditLogger: auditLogger,
SamlService: samlService,
Persister: persister,
Cfg: *cfg,
PasscodeService: passcodeService,
PasswordService: passwordService,
WebauthnService: webauthnService,
SessionManager: sessionManager,
PasscodeRateLimiter: passcodeRateLimiter,
PasswordRateLimiter: passwordRateLimiter,
TokenExchangeRateLimiter: tokenExchangeRateLimiter,
AuthenticatorMetadata: authenticatorMetadata,
AuditLogger: auditLogger,
SamlService: samlService,
}
if cfg.Saml.Enabled {

View File

@@ -81,6 +81,14 @@ func Limit2(store limiter.Store, key string) (int, bool, error) {
return retryAfterSeconds, ok, nil
}
func CreateRateLimitKey(realIP, email string) string {
return fmt.Sprintf("%s/%s", realIP, email)
func CreateRateLimitPasscodeKey(realIP, email string) string {
return fmt.Sprintf("passcode/%s/%s", realIP, email)
}
func CreateRateLimitPasswordKey(realIP, userId string) string {
return fmt.Sprintf("password/%s/%s", realIP, userId)
}
func CreateRateLimitTokenExchangeKey(realIP string) string {
return fmt.Sprintf("token_exchange/%s", realIP)
}