Authentication
How users prove who they are — credentials, password hashing, JWT access tokens, and the underlying threat model.
The Auth authenticates users with password-based credentials and emits HS256-signed JWT access tokens plus opaque refresh tokens. Every other authentication mode (OTP, OAuth, magic links) is on the roadmap; today the implemented surface is password-based login.
Credentials
Users authenticate with one identifier from any of three:
| Identifier | Notes |
|---|---|
email | Standard email address. |
username | Optional handle, unique per tenant. |
cpf_cnpj | Brazilian individual or corporate document, unique per tenant. |
Tenants enable whichever combination they want. The POST /login body simply
receives identifier — the service figures out which field it matches against.
Passwords are hashed at rest with bcrypt, 12 rounds (OWASP 2025 baseline) and are never echoed back in any API response.
Login flow
Client sends credentials
POST /api/v1/login
Content-Type: application/json{
"tenantCode": "febasi",
"identifier": "you@febasi.com.br",
"password": "..."
}Rate-limited to 5 requests per minute per IP, so brute-force attacks hit the wall before they hit the database.
Server resolves the tenant
The tenantCode is looked up. If missing or INACTIVE, the response is
TENANT_NOT_FOUND — and importantly, this happens before any password
comparison so timing does not leak whether a tenant exists.
Server compares the password
If the user does not exist, the service still runs a dummy bcrypt comparison to match the timing of a real failed login. This blunts user-enumeration attacks: an attacker cannot tell from response time whether the identifier exists.
If the password matches, the user is loaded with their roles, permissions, and tenant data.
Server issues tokens
{
"success": true,
"data": {
"accessToken": "<jwt>",
"refreshToken": "<uuid>",
"expiresIn": 900,
"user": { "id": "...", "tenantCode": "febasi", "...": "..." }
}
}The access token
The access token is a JWT signed with HS256 using either the global
JWT_SECRET or a per-tenant secret if one is configured. Default expiration
is 15 minutes, configurable per tenant.
{
"sub": "01HXY...", // user id
"userId": "01HXY...", // alias
"tenantId": "01HX0...",
"tenantCode": "febasi",
"email": "you@febasi.com.br",
"username": null,
"roles": ["super_admin"],
"permissions": ["users:read", "users:create", "..."],
"iat": 1714521600,
"exp": 1714522500
}Permissions live inside the JWT
The Auth embeds the user's effective permissions in the token so that
downstream services can authorize requests without a database round-trip.
The trade-off: permission changes take effect on the next access-token
refresh, not instantly. For permission checks that must be real-time,
call POST /permissions/check against Febasi Auth directly.
What is not yet implemented
The schema and roadmap support the items below; they are tracked for upcoming releases. Until shipped, they are not exposed publicly:
- OTP / TOTP 2FA —
users.otp_enabledandusers.otp_secretexist; no public flow yet. - Email verification —
users.email_verifiedfield exists; no verification endpoint yet. - Password reset —
password_resetstable exists; no public flow yet. - OAuth providers — Google, GitHub, etc. are tracked but not wired in.
- Magic links / passkeys — not on the current roadmap.
What about machine-to-machine?
For service-to-service authentication, use Client Keys — see Client Keys. They authenticate without a username/password and carry their own scoped permissions.