Febasi Docs
Guides

Login flow in depth

Every branch of POST /login — happy path, password policy, brute-force protection, and the 401 vs 404 distinction.

This guide walks through POST /api/v1/login end-to-end. If you're after the five-minute introduction, see Getting started; this page is for the day you need to debug the unhappy path.

Request

POST /api/v1/login
Content-Type: application/json

Prop

Type

Happy path response

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "8c9a3a48-7b9d-...",
    "expiresIn": 900,
    "user": {
      "id": "01HXY...",
      "email": "you@febasi.com.br",
      "username": null,
      "tenantId": "01HX0...",
      "tenantCode": "febasi"
    }
  }
}

expiresIn is the access-token lifetime in seconds; defaults to 900 (15 minutes) unless the tenant overrides it via auth_config.

Branches of failure

The Auth API distinguishes carefully between what failed — clients can react differently to each.

CodeHTTPWhen
TENANT_NOT_FOUND404The tenant code does not exist or is INACTIVE.
INVALID_CREDENTIALS401Identifier not found or password mismatch.
VALIDATION_ERROR400Body shape is wrong (Zod validation).
PASSWORD_POLICY_VIOLATION400Password fails the tenant's policy on register (login itself only verifies the hash).
RATE_LIMIT_EXCEEDED429More than 5 logins/min from the same IP.

Why one error for two failures?

INVALID_CREDENTIALS is returned for both "user does not exist" and "wrong password". This is intentional: it prevents user enumeration. Combined with the dummy bcrypt comparison run when the user is missing, neither the response body nor the response time leaks whether an identifier exists in the tenant.

Brute-force protection

POST /login is rate-limited to 5 requests per minute per IP. The limit is enforced on the Fastify side (in-memory by default, Redis-backed when configured). Hitting the limit returns:

{ "success": false, "error": "Rate limit exceeded", "code": "RATE_LIMIT_EXCEEDED" }

Beyond rate limits, the security metrics endpoint (GET /api/v1/tenants/me/metrics/security) surfaces brute-force indicators:

  • Failed attempts in the last 5 minutes.
  • Unique IPs hitting failed logins.
  • IPs with ≥ 5 failures in 24 hours (flagged as suspicious).

You can wire alerts on top of this — the threshold for "possible brute force" is >10 failures in a 5-minute window.

Edge cases worth knowing

What you receive in the JWT

The access token already carries:

  • userId, tenantId, tenantCode
  • The user's email and username (whichever they have)
  • A flat array of roles (role names)
  • A flat array of permissions (effective permissions, deduped)

So most consumers do not call /me after login — they decode the JWT for display and route protection. /me exists for clients that need to refresh the user state without rotating tokens (e.g., to pick up a new role).

Next steps

On this page