sequenceDiagram
participant User
participant SPA as Cloud SPA
participant API as Cloud Backend
participant DB as Azure SQL
User->>SPA: Visit /login
User->>SPA: Enter email
SPA->>API: POST /api/v1/auth/check-method { email }
API->>DB: SELECT auth_policy, sso_config WHERE tenant matches email domain
DB-->>API: { method: "password" | "sso" }
API-->>SPA: { method: "password" }
User->>SPA: Enter password
SPA->>API: POST /api/v1/auth/login { email, password }
API->>DB: SELECT user by email
API->>API: bcrypt.Compare(password, hash)
API->>DB: SELECT tenant_licenses (check active)
alt MFA enabled
API-->>SPA: { mfa_required: true, session_token }
User->>SPA: Enter TOTP code
SPA->>API: POST /api/v1/auth/mfa/verify { totp_code, session_token }
API->>API: Verify TOTP (pquerna/otp)
end
API->>API: Sign access JWT (60 min)
API->>DB: INSERT refresh_tokens
API-->>SPA: { access_token } + Set-Cookie: refresh_token (HttpOnly)
SPA->>SPA: authStore.setAccessToken(access_token)
SPA->>API: GET /api/v1/me
API-->>SPA: { user, permissions, features }
SPA->>SPA: authStore.setUser(...)
SPA-->>User: Redirect to /dashboard
sequenceDiagram
participant User
participant SPA as Cloud SPA
participant API as Cloud Backend
participant Entra as Microsoft Entra ID
participant DB as Azure SQL
User->>SPA: Click "Sign in with Microsoft"
SPA->>API: GET /api/v1/auth/microsoft/login
API-->>User: 302 → Entra ID authorize URL
User->>Entra: Authenticate (corporate credentials)
Entra-->>API: GET /api/v1/auth/microsoft/callback?code=...
API->>Entra: Token exchange (code → ID token)
Entra-->>API: ID token { email, oid, name }
API->>DB: Find user by email (or create)
API->>DB: Check tenant_licenses
API->>API: Issue JWT pair
API-->>User: 302 → {APP_BASE_URL}/auth/callback?token=<access_token>
User->>SPA: /auth/callback
SPA->>SPA: Extract token from URL, store in authStore
SPA->>API: GET /api/v1/me
SPA-->>User: Redirect to /dashboard
sequenceDiagram
participant User
participant SPA as Cloud SPA (SessionRestore)
participant API as Cloud Backend
User->>SPA: F5 (full reload)
SPA->>SPA: Zustand hydrates: user set, access_token null
SPA->>SPA: Wait for hasHydrated()
SPA->>API: POST /api/v1/auth/refresh (cookie sent automatically)
alt Refresh token valid
API->>API: Validate JWT, check DB blacklist
API->>API: Issue new access JWT
API-->>SPA: { access_token }
SPA->>SPA: authStore.setAccessToken(new_token)
SPA-->>User: Render protected page
else Expired or missing
API-->>SPA: 401
SPA->>SPA: authStore.clearAuth()
SPA-->>User: Redirect to /login
end
sequenceDiagram
participant User
participant SPA as Cloud SPA
participant API as Cloud Backend
participant DB as Azure SQL
User->>SPA: Click Logout
SPA->>API: POST /api/v1/auth/logout
API->>DB: DELETE or invalidate refresh_token
API-->>SPA: 200 + Set-Cookie: refresh_token= (cleared)
SPA->>SPA: authStore.clearAuth()
SPA-->>User: Redirect to /login