Febasidocs
Concepts

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:

IdentifierNotes
emailStandard email address.
usernameOptional handle, unique per tenant.
cpf_cnpjBrazilian 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 2FAusers.otp_enabled and users.otp_secret exist; no public flow yet.
  • Email verificationusers.email_verified field exists; no verification endpoint yet.
  • Password resetpassword_resets table 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.

On this page