REF-701-02: Authentication Flow
ADR: ADR-701 — API & AuthenticationEscopo: Login/refresh flow, payload JWT, TTL, rate limiting, validação de senha, nonce interno, OAuth 2.0 portal
1. Login Flow
POST /wp-json/middag-account/v1/auth/login
Body: { "email": "user@example.com", "password": "********" }
Validação:
├── Email existe?
├── Senha válida?
├── Rate limit ok? (5/min/IP)
└── Organização verificada?
Sucesso (200):
{
"success": true,
"data": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 86400
}
}
Falha (401):
{
"success": false,
"errors": { "code": "AUTHENTICATION_ERROR", "detail": "Invalid credentials" }
}2. Refresh Flow
POST /wp-json/middag-account/v1/auth/refresh
Body: { "refresh_token": "eyJ..." }
Validação:
├── Refresh token válido?
├── Não expirado (7 dias)?
└── Não revogado?
Sucesso (200):
{
"success": true,
"data": {
"access_token": "eyJ... (novo)",
"token_type": "Bearer",
"expires_in": 86400
}
}3. Payload JWT (Access Token)
{
"sub": 42,
"org": 15,
"roles": ["admin"],
"scopes": ["organization", "finances", "orders", "licenses", "tickets"],
"company": "middag_br",
"iat": 1714200000,
"exp": 1714286400
}| Campo | Tipo | Descrição |
|---|---|---|
sub | int | User ID (WordPress) |
org | int | Organization ID |
roles | string[] | Roles do collaborator na org |
scopes | string[] | Scopes atribuídos |
company | string | Company context default |
iat | int | Issued at (Unix timestamp) |
exp | int | Expiration (Unix timestamp) |
Decisão: Campos sub, org, roles, scopes no nível superior — NÃO aninhados. Alinha com padrão JWT (bloqueador #7 resolvido).
4. TTL
| Token | TTL | Renovável |
|---|---|---|
| Access | 24h | Sim (via refresh) |
| Refresh | 7 dias | Não (requer login) |
5. Algoritmo e Key Management
| Aspecto | Detalhe |
|---|---|
| Algoritmo | RS256 (RSA + SHA-256) |
| Nunca HS256 | HS256 compartilha secret — RS256 usa keypair |
| Private key | Fora do webroot (não acessível via HTTP) |
| Public key | Disponível para validação por serviços externos |
| Rotação | Manual (admin). Tokens existentes invalidados. |
6. Rate Limiting
| Endpoint | Limite | Janela | Resposta excedida |
|---|---|---|---|
/auth/login | 5 requests | 1 min | 429 RATE_LIMIT |
/auth/refresh | 10 requests | 1 min | 429 RATE_LIMIT |
/auth/register | 3 requests | 1 min | 429 RATE_LIMIT |
Baseado em IP. Header Retry-After incluído na resposta 429.
7. Validação de Senha
| Regra | Requisito |
|---|---|
| Comprimento mínimo | 8 caracteres |
| Complexidade | Maiúscula + minúscula + número |
| Caracteres especiais | Não obrigatórios (recomendados) |
Bloqueador #6 resolvido.
8. Autenticação por Contexto
| Contexto | Método | Quando |
|---|---|---|
| Portal (NextJS) | JWT RS256 (access + refresh) | Login do cliente |
| wp-admin (Inertia) | WordPress nonce (wp_verify_nonce) | Chamadas internas do admin |
| WC API Keys | Consumer key + secret (WC nativo) | Integrações WooCommerce diretas |
| Satélites (PHP) | WordPress nonce (contexto admin) | Chamadas server-to-server internas |
9. OAuth 2.0 para Portal (Futuro v5.0.x)
| Provider | Suportado | Notas |
|---|---|---|
| Sim | OAuth 2.0 standard | |
| Azure AD | Sim | Para clientes enterprise com Azure |
| SAML 2.0 | Não | Removido na reorganização — OAuth cobre 90% |
Flow: Provider OAuth → callback → vincular a WordPress user → emitir JWT middag-account.
10. Middleware Chain
Every authenticated request passes through the following middleware pipeline in order:
HTTP Request
|
v
+-------------------------------------+
| 1. AuthMiddleware::getCurrentUser() |
| Extract and validate JWT |
| Set WP_User in request context |
+-------------------------------------+
|
v
+-------------------------------------+
| 2. PermissionsMiddleware |
| Validate role and scopes |
| (if route is protected) |
+-------------------------------------+
|
v
+-------------------------------------+
| 3. OrganizationMiddleware |
| Validate X-Middag-Organization |
| (if route is org-scoped) |
+-------------------------------------+
|
v
+-------------------------------------+
| 4. CompanyMiddleware |
| Validate X-Middag-Company |
| (if route is company-scoped) |
+-------------------------------------+
|
v
Controller processes the request10.1 AuthMiddleware Detail
1. Extract token from Authorization header (regex: /^Bearer\s+(.+)$/)
If absent -> 401 "Token not provided"
2. Decode JWT header (base64url), verify alg === "RS256"
If different -> 401 "Unsupported algorithm"
3. Validate RS256 signature using RSA public key
If invalid -> 401 "Invalid signature"
4. Check expiration (exp)
If exp < time() -> 401 "Token expired"
5. Check issuer (iss)
If iss !== expected issuer -> 401 "Invalid issuer"
6. Load WP_User by sub (user ID)
If not found -> 401 "User not found"
7. Check last_logout
If last_logout > iat -> 401 "Token revoked by logout"
8. Set user in request context: wp_set_current_user(user_id)10.2 PermissionsMiddleware Detail
1. Get route definition -> extract required_role and required_scopes
2. If org-scoped: load CollaboratorModel for user + org
If not a collaborator -> 403 "No access to organization"
3. Check minimum role
Hierarchy: owner > admin > member > guest > pending
If role < required_role -> 403 "Insufficient permission"
4. Check scopes
Collaborator must have all required scopes
Wildcard "*" grants all scopes
If scope missing -> 403 "Scope not authorized: {scope}"10.3 OrganizationMiddleware Detail
1. Extract org_id from X-Middag-Organization header
If absent -> 400 "Header X-Middag-Organization required"
2. Validate organization exists
If not found -> 404 "Organization not found"
3. Validate user is a collaborator
If not a member -> 403 "No access to organization"
4. Set organization context in request11. Login Validations (WordPress Side)
| Step | Validation | Failure |
|---|---|---|
| 1 | Email present and valid format | 400 Bad Request |
| 2 | Password present | 400 Bad Request |
| 3 | wp_authenticate() returns WP_User | 401 Unauthorized |
| 4 | Account not blocked | 403 Forbidden |
| 5 | Email verified (email_verified meta) | 403 Forbidden |
| 6 | Rate limit not exceeded (5/min per IP) | 429 Too Many Requests |
12. Refresh Token Rotation
The refresh token implements mandatory rotation:
- Each use of the refresh token generates a new pair (access + refresh)
- The previous refresh token is immediately invalidated
- If an already-invalidated refresh token is used (replay attack), all tokens for the user are revoked
- The new refresh token has a 7-day validity from issuance
Normal use:
RT-1 -> (new AT-2, new RT-2) -> RT-1 invalidated
RT-2 -> (new AT-3, new RT-3) -> RT-2 invalidated
Replay attack detected:
RT-1 already used -> REVOKE ALL -> Force re-login12.1 Refresh Validation Steps
| Step | Validation | Failure |
|---|---|---|
| 1 | Refresh token present in body | 400 Bad Request |
| 2 | Token found in user_meta (hash match) | 401 Unauthorized |
| 3 | Token not expired (< 7 days) | 401 Unauthorized |
| 4 | User has not logged out (last_logout < token creation) | 401 Unauthorized |
| 5 | User account is active | 403 Forbidden |
13. OAuth 2.0 Flows (Portal)
OAuth is handled by the NextJS portal via NextAuth.js. WordPress provides the bridge endpoint /auth/register-email.
13.1 Google OAuth Flow
App (Browser) NextJS Google OAuth WordPress API
|
| Click "Login with Google"
|
|---> NextAuth signIn('google')
|
| Redirect to Google authorize
|---> Google login page
| User authenticates + consents
|<--- Callback with auth code
|
| Exchange code for tokens
|---> POST /oauth2/token
|<--- access_token, id_token
|
| Extract profile from id_token
| (email, name, picture)
|
| POST /middag-account/v1/auth/register-email
| Body: { email, name, avatar, provider: "google" }
| |
| | Find/create WP_User
| | Mark email_verified = true
| | Generate JWT + refresh
|<-------------------------------------|
|
| Store tokens in NextAuth session
|<---
| Session cookie set, redirect /homeGoogle provider config: Client ID/Secret via env vars, scopes: openid email profile, callback: https://app.middag.io/api/auth/callback/google.
13.2 Microsoft (Azure AD) OAuth Flow
Same structure as Google with Azure AD endpoints:
- Authorization:
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize - Token:
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token - Scopes:
openid email profile User.Read - Profile fields:
preferred_username(email),displayName(name)
Azure AD provider config: Client ID/Secret/Tenant ID via env vars, callback: https://app.middag.io/api/auth/callback/azure-ad.
14. NextAuth Session Cookie
| Property | Value | Description |
|---|---|---|
| Name | next-auth.session-token | Session cookie name |
| Strategy | jwt | Session stored in cookie as JWT (no database) |
| Max Age | 86400 (24 hours) | Cookie lifetime |
| Secure | true (production) | Cookie only sent via HTTPS |
| HttpOnly | true | Inaccessible via JavaScript (XSS protection) |
| SameSite | lax | Partial CSRF protection (allows normal navigation) |
| Path | / | Cookie available on all routes |
The NextAuth JWT callback runs on every request and manages token lifecycle:
- Initial login: Stores access_token, refresh_token, expires_at, and user data
- Token still valid: Returns token unchanged
- Token expired: Calls
/auth/refresh, updates tokens; on failure setserror: 'RefreshAccessTokenError' - Client-side: If session has
RefreshAccessTokenError, redirects to login
15. Token Invalidation Scenarios
| Event | Mechanism | Effect |
|---|---|---|
| Voluntary logout | Updates last_logout in user_meta | All JWTs issued before logout are rejected |
| Password change | Updates last_logout + revokes refresh tokens | Forces re-login on all devices |
| Admin disables account | Flag account_disabled in user_meta | AuthMiddleware rejects any token |
| Refresh token replay | Detects already-used token | Revokes ALL tokens for the user |
| Collaborator removal | Removes CollaboratorModel record | PermissionsMiddleware blocks access to that org |
15.1 last_logout Mechanism
The last_logout field in user_meta acts as a mass invalidation mechanism:
JWT issued at: iat = 1735689600
User logged out at: 1735700000
JWT used at: 1735710000
Check: last_logout (1735700000) > iat (1735689600) -> TOKEN REJECTEDThis ensures that even JWTs still within their 24h validity window are rejected after logout, without needing a centralized blacklist.
16. Security Protections
| Protection | Implementation | Layer |
|---|---|---|
| CSRF | SameSite=lax cookie + state parameter (OAuth) | NextAuth |
| XSS | HttpOnly cookie (tokens inaccessible via JS) | NextAuth |
| Token theft | RS256 (asymmetric signature), refresh rotation | WordPress |
| Brute force | Rate limit 5 attempts/min per IP | WordPress |
| Replay attack | JTI claim in JWT + refresh token rotation | WordPress |
| Session fixation | New token on each refresh | WordPress+NextAuth |
| Man-in-the-middle | HTTPS mandatory, Secure cookie flag | Infrastructure |
16.1 Extended Rate Limiting
| Endpoint | Limit | Window | Per |
|---|---|---|---|
/auth/login | 5 | 1 minute | IP |
/auth/register | 3 | 1 minute | IP |
/auth/forgot-password | 3 | 5 minutes | IP |
/auth/resend-verification | 3 | 5 minutes | |
/auth/refresh | 10 | 1 minute | User ID |
| Other authenticated endpoints | 60 | 1 minute | User ID |
16.2 Password Requirements
| Requirement | Value |
|---|---|
| Minimum length | 8 characters |
| Uppercase letters | At least 1 |
| Lowercase letters | At least 1 |
| Numbers | At least 1 |
| Special characters | Recommended (not required) |
17. Role Hierarchy
owner ---- Full access, can delete org
|
+-- admin ---- Manage members (member/guest), all operations per scopes
|
+-- member ---- Operations per assigned scopes
|
+-- guest ---- Restricted read access per assigned scopes
|
+-- pending ---- Invite not accepted, no access| Role | Can manage members | Can alter org | Can delete org | Operations access |
|---|---|---|---|---|
| owner | Yes | Yes | Yes | All (implicit) |
| admin | Yes (member/guest) | Yes (if scope) | No | Per assigned scopes |
| member | No | No | No | Per assigned scopes |
| guest | No | No | No | Restricted read per scopes |
| pending | No | No | No | None (invite not accepted) |
18. SAML 2.0 Removal
SAML 2.0 has been removed from the v4/v5 architecture. The Jira SSO is now handled directly by the NextJS portal using a dedicated integration. Removed dependencies: simplesamlphp/simplesamlphp, robrichards/xmlseclibs. No SAML classes, configs, metadata, or certificates exist in the plugin.