Skip to main content

Authentication

Flo supports several authentication flows. All result in a cookie-based session (FloAuth, HttpOnly, Secure, SameSite=Lax).

Auth Flows​

1. Password-Based Login​

Traditional email/password authentication with brute force protection.

  • Lockout: 5 failed attempts triggers a 15-minute lockout
  • Password hashing: BCrypt
  • Session: Cookie set on successful login

Flow:

User → POST /api/v1/auth/login { email, password }
→ Server validates credentials
→ Server checks lockout counter
→ Cookie set → Redirect to dashboard

Accounts without a password (OTP/social) are rejected before the BCrypt check. Every login also passes LoginAccessPolicy: the account must be enabled, enable_login must be ON for non-privileged users, and AdminOnlyLogin blocks non-admin/pro accounts.

2. OTP Email Login​

Passwordless login via a 6-digit code sent by email.

  • Code length: 6 digits
  • Expiry: 30 minutes
  • Max attempts: 3 per code
  • Cooldown: 30 seconds between OTP requests (per email; configurable via Otp:MinResendIntervalSeconds, never below 30 seconds in production)

Flow:

User → POST /api/v1/public/auth/otp/request { email, mode }
→ Server generates 6-digit code and returns a request id
→ Email sent with code
→ User enters code
→ POST /api/v1/public/auth/otp/validate { email, code, requestId }
→ Cookie set → Redirect to dashboard

mode=register creates the account and is gated by enable_registration; login-mode requests are anti-enumerated (an ineligible address gets a decoy response). /api/v1/public/auth/otp/resend re-sends within the cooldown policy.

3. OIDC/OAuth2 (OpenIddict)​

Full OAuth2 server for external clients (e.g., blog CMS, third-party apps).

  • Grant type: Authorization Code with PKCE
  • Refresh tokens: Supported
  • Scopes: openid, profile, email, roles
  • Certificate-based: Signing and encryption certs configured per tenant

Flow:

Client → GET /connect/authorize (with PKCE challenge)
→ User authenticates (password or OTP)
→ Authorization code returned
→ POST /connect/token (exchange code for tokens)
→ Access token + Refresh token

4. Social Login (Google / Apple)​

Anonymous endpoints under /api/v1/external-auth:

EndpointPayload
GET /configPublic provider config (which buttons render, Client IDs)
POST /google{ credential, nonce? } — Google ID token
POST /apple{ idToken, nonce?, firstName?, lastName? }
  • A provider is exposed only when its feature flag (enable_google_login / enable_apple_login) is ON and a Client ID is configured; otherwise the login screen does not render the button.
  • Settings are managed by SuperAdmin in Settings > provider access (impostazioni/login-providers) and stored in the ExternalAuthSettings table (Client ID + Apple redirect URI).
  • The ID token is verified against the provider's JWKS with the stored Client ID as audience (OidcTokenVerifier).
  • User resolution: returning identity by (provider, sub); auto-link when the provider-verified email matches an existing account; otherwise a new password-less account is created only when enable_registration is ON and the email is verified.
  • Social sessions honour the same login kill-switches as password login (LoginAccessPolicy) and are issued through AuthCookieHelper, so the cookie and claims are identical.

5. Registration & Email Confirmation​

POST /api/v1/auth/register requires enable_login and enable_registration ON. Registration creates the user with IsEnabled = false and sends a confirmation email (skipped when no email provider is configured); no session is issued.

EndpointPurpose
POST /api/v1/auth/enable-user { email, token }Confirms the email and enables the account
POST /api/v1/auth/set-password { email, token, newPassword }Sets the password for invited accounts and enables them
POST /api/v1/auth/resend-confirmationAdmin-gated re-send of the confirmation email
POST /api/v1/auth/resend-password-creationAdmin-gated re-send of the password-creation email
POST /api/v1/auth/forgot-password / POST /api/v1/auth/reset-passwordPassword recovery

6. Forced First-Access Password Change​

The seeded SuperAdmin operator accounts start with known default passwords. LoginAccessMiddleware keeps such a session out of every endpoint with 403 errors.auth.forcedPasswordChangeRequired, except the allowlist marked with [AllowWhenDefaultPassword]:

  • GET /api/v1/auth/verify-session, POST /api/v1/auth/logout
  • GET /api/v1/users/me/password-status, PUT /api/v1/users/me/preferred-content-language
  • POST /api/v1/users/{id}/change-password and GET /api/v1/users/{id} — only when {id} is the caller's own id

The verdict is cached (30 seconds) by DefaultPasswordGate; the cache key includes SessionsInvalidatedAt, so a password write invalidates it automatically. On the frontend, DefaultPasswordGuard redirects dashboard navigation to /auth/forced-password-change, and the login flow skips dashboard data loads while the gate is active. The change uses POST /api/v1/users/{id}/change-password with the caller's own id; the write revokes the session and sends the user back to the login. The gate has no feature flag and is always on (spec docs/specs/2026-09-22-forced-password-change.md).

Session Management​

All auth flows result in the same cookie:

PropertyValue
NameFloAuth (per-tenant COOKIE_NAME=FloAuth-<slug> override)
HttpOnlytrue
Securetrue (production; same-as-request in development)
SameSiteLax
Expiry30 days, sliding
DomainConfigurable via COOKIE_DOMAIN env var (dot-prefixed in production)

Cookie sessions carry an issued-at claim (flo_auth_issued_at) and are capped by an absolute lifetime of 7 days, independent of the sliding expiry. The frontend restores the session on startup by calling GET /api/v1/auth/verify-session and then GET /api/v1/users/me/password-status.

Session Invalidation​

ScopeMechanismExposed via
One userAuthSessionService.InvalidateUserAsync stamps User.SessionsInvalidatedAt and revokes that user's OpenIddict tokensPassword change/reset (same transaction), logout
All usersInvalidateAllAsync stamps the singleton AuthSessionState.InvalidatedAt and revokes all OpenIddict tokensPOST /api/v1/admin/invalidate-sessions (Admin; also signs out the caller), POST /api/v1/maintenance/auth/invalidate-sessions (maintenance key)

LoginAccessMiddleware rejects a cookie session whose issued-at timestamp is older than the tenant or user invalidation timestamp. Legacy cookies without the claim are accepted and re-stamped once.

Role Claim Refresh​

The role is baked into the session cookie as a bitmask at login. On every authenticated request LoginAccessMiddleware compares the privilege bits (Pro | Admin | SuperAdmin) in the database with the cookie: when they differ (a grant or a revocation), the cookie is re-issued with the current claims before the request reaches the controllers. A re-issue reuses the original ticket properties, so the 30-day expiry is untouched. OTP/OIDC bearer tokens are not affected — they do not use the cookie (spec docs/specs/2026-09-17-refresh-role-claims.md).

Claims​

Different auth methods use different claim types for the user ID:

Auth MethodClaim Type
Cookie (password)ClaimTypes.NameIdentifier
OTP / OpenIddictClaims.Subject

Backend services handle both claim types when resolving the current user.

Cookie sessions also carry the role bitmask in ClaimTypes.Role and the is_admin / is_super_admin / is_pro flags. AuthCookieHelper.BuildSessionClaims is the single source used by password login, social login and the per-request role refresh.

Brute-Force Protection​

  • Failed password attempts are recorded per email in the LoginAttempt table; 5 failures lock the account for 15 minutes. A locked attempt returns too-many-attempts with the remaining minutes; a successful login clears the counter, and the counter restarts after the window elapses.
  • Unknown emails still pay a BCrypt verify (dummy hash) so the response time does not reveal account existence.
  • Password-less accounts never write a lockout row: they are rejected before the password check.
  • Auth events are recorded for login successes and failures (AuthEventService), with emails masked.

Key Files​

FilePurpose
Flo.BE/Services/AuthService.csPassword login, registration, email confirmation, brute force protection
Flo.BE/Services/OtpAuthService.csOTP code generation and verification
Flo.BE/Services/ExternalAuthService.csGoogle/Apple sign-in and account linking
Flo.BE/Services/ExternalAuthSettingsService.csProvider Client IDs and public config
Flo.BE/Services/AuthSessionService.csSession invalidation and lifetime checks
Flo.BE/Services/DefaultPasswordGate.csForced-password-change verdict cache
Flo.BE/Middlewares/LoginAccessMiddleware.csPer-request session, role and forced-password checks
Flo.BE/Helpers/AuthCookieHelper.csSingle source of session cookie claims
Flo.BE/Controllers/AuthController.csLogin, register, confirmation, password reset
Flo.BE/Controllers/ExternalAuthController.csSocial sign-in endpoints
Flo.BE/Controllers/OAuthController.csOIDC authorization and token endpoints
Flo.FE/src/app/guards/auth.guard.tsFrontend route protection
Flo.FE/src/app/guards/default-password.guard.tsForced password change redirect
Flo.FE/src/app/services/global.store.tsSession restore on app init