mirror of
https://github.com/teamhanko/hanko.git
synced 2026-03-13 08:43:15 +08:00
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:
@@ -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 (
|
||||
|
||||
@@ -164,5 +164,6 @@ func DefaultConfig() *Config {
|
||||
MinLength: 3,
|
||||
MaxLength: 40,
|
||||
},
|
||||
Debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user