Authentication endpoints
Authentication endpoints
The Authentication API exposes the five endpoints under /api/v1/auth that drive the JWT register / login / refresh / logout flow used by the e-bon Portal and the E-BON mobile app. These are the only endpoints in the API that a brand-new caller can hit without already holding credentials — register, login, forgot-password and refresh are public; logout requires a valid Portal JWT so the server knows whose refresh token to revoke.
/api/v1/auth/* routes.The whole /api/v1/auth surface is protected by a stricter rate limit than the rest of the API. See Token lifetimes & rate limits below for the actual numbers. The error envelope, idempotency rules and pagination conventions are documented once on API overview; only the per-endpoint error codes are listed below.
POST /api/v1/auth/register
Creates a brand-new organization and the Owner user that owns it: provisions a Firebase Auth user, writes the organizations/{orgId} and organizations/{orgId}/users/{uid} documents, optionally seeds a first location from the supplied ANAF address, sets Firebase custom claims (orgId, role) for Firestore security rules, and returns a fresh access + refresh token pair.
Auth: Public — no authentication required.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Valid email address. Becomes the Firebase Auth login. |
password | string | yes | Minimum 8 characters. |
companyName | string | yes | 1–255 chars. Used as both the organization name and the Firebase Auth displayName. |
cui | string | yes | Romanian fiscal code, max 20 chars. Must match ^(RO)?\d{2,10}$ (e.g. RO12345678 or 12345678). |
address | string | no | Up to 500 chars. When provided, the server parses it via parseAnafAddress and seeds both billingAddress and a first location. |
Response 201
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"userId": "user_xyz",
"orgId": "acme_corp"
}
The new user is created with role owner. Use the accessToken immediately as Authorization: Bearer <jwt> on subsequent calls; persist the refreshToken securely so you can call POST /api/v1/auth/refresh once the access token expires.
Errors
VALIDATION_ERROR(400) — body failed Zod validation (missing field, password shorter than 8 chars, malformedcui, oversized field).CONFLICT(409) —An account with this email already exists— Firebase Auth refused withauth/email-already-exists.RATE_LIMIT_EXCEEDED(429) — auth rate-limit window tripped. Retry after theRetry-Afterseconds.INTERNAL_ERROR(500) —Failed to create user account(unexpected Firebase Auth error) orFailed to create organization(Firestore batch write failed; the partially-created Firebase Auth user is rolled back automatically).
The full HTTP catalogue is on API overview › HTTP error code catalogue and per-code recovery steps live on the errors reference.
Example
curl -X POST https://api.e-bon.ro/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "owner@acme.example",
"password": "a-strong-password",
"companyName": "Acme Corp SRL",
"cui": "RO12345678",
"address": "Str. Lipscani 12, sector 3, București"
}'
POST /api/v1/auth/login
Authenticates with email + password against Firebase Auth and returns a fresh access + refresh token pair. The handler resolves orgId and role from the user's Firebase custom claims; if those are missing it falls back to a collectionGroup('users') query and lazily writes the resolved claims back so the next login is faster.
Auth: Public — no authentication required.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Valid email address. |
password | string | yes | Non-empty. |
Response 200
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"userId": "user_xyz",
"orgId": "acme_corp"
}
The refresh token is stored on the server as a SHA-256 hash under organizations/{orgId}/users/{uid}/refreshTokens so it can later be revoked by /auth/logout or rotated by /auth/refresh.
Errors
VALIDATION_ERROR(400) — body failed Zod validation.UNAUTHORIZED(401) —Invalid email or password(returned uniformly for unknown email, wrong password, or a Firebase Auth user without a Firestore membership — the response body never reveals which case applies).RATE_LIMIT_EXCEEDED(429) — auth rate-limit window tripped.INTERNAL_ERROR(500) —Authentication service misconfiguredwhen the server is missingFIREBASE_WEB_API_KEYand is not pointed at the Firebase Auth emulator.
Example
curl -X POST https://api.e-bon.ro/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "owner@acme.example",
"password": "a-strong-password"
}'
POST /api/v1/auth/forgot-password
Asks Firebase Identity Toolkit to send the password-reset email for the given address. The handler always returns 200 — even when the email does not exist or the upstream Firebase call fails — so callers cannot enumerate accounts by probing this endpoint.
Auth: Public — no authentication required.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Valid email address. |
Response 200
{
"message": "If an account with that email exists, a password reset link has been sent."
}
The same response body is returned whether or not the email matched a real Firebase Auth user; failures upstream are logged on the server but suppressed from the response.
Errors
VALIDATION_ERROR(400) — body failed Zod validation (missing or malformedemail).RATE_LIMIT_EXCEEDED(429) — auth rate-limit window tripped.
FIREBASE_WEB_API_KEY configured the request is silently ignored — the client still receives the 200 envelope above, but no email is dispatched. Operators should check the API logs for FIREBASE_WEB_API_KEY not configured — forgot-password request ignored if reset emails never arrive.Example
curl -X POST https://api.e-bon.ro/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{ "email": "owner@acme.example" }'
POST /api/v1/auth/refresh
Exchanges a still-valid refresh token for a new access + refresh token pair. The handler verifies the JWT signature, checks that the SHA-256 hash of the refresh token is still present under organizations/{orgId}/users/{uid}/refreshTokens (i.e. that the token has not been revoked), then rotates the refresh token: the previous Firestore document is deleted and a fresh hash is stored. The old refresh token cannot be reused after a successful refresh.
Auth: Public — no authentication required (the refresh token in the body is the credential).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
refreshToken | string | yes | Non-empty. The refresh token from /login, /register or a previous /refresh. |
Response 200
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi..."
}
Note that — unlike /login and /register — this response does not include userId or orgId. The new refresh token replaces the one you sent; persist it and discard the old one.
Errors
VALIDATION_ERROR(400) — body failed Zod validation (missing or emptyrefreshToken).UNAUTHORIZED(401) —Invalid or expired refresh token(signature, issuer or TTL check failed) orRefresh token has been revoked(the hash is no longer in Firestore — typically because/logoutdeleted it or because a previous/refreshalready rotated it).RATE_LIMIT_EXCEEDED(429) — auth rate-limit window tripped.
Example
curl -X POST https://api.e-bon.ro/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "eyJhbGciOi..." }'
POST /api/v1/auth/logout
Revokes the supplied refresh token by deleting the matching SHA-256 hash from organizations/{orgId}/users/{uid}/refreshTokens. The currently-issued access token is not invalidated — it remains valid until its short TTL expires; relying on logout for hard cut-off requires waiting that window out.
Auth: Portal JWT, any authenticated user.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
refreshToken | string | yes | Non-empty. The refresh token to revoke. |
Response 200
{ "message": "Logged out successfully" }
The handler returns 200 even when the supplied refresh token is no longer in Firestore — logout is idempotent, so a duplicate logout does not error. Sending a refresh token that belongs to a different user is allowed by the route but harmless: only documents under the caller's own {orgId}/{userId} path are scanned.
Errors
VALIDATION_ERROR(400) — body failed Zod validation.UNAUTHORIZED(401) — missing or invalid Portal JWT.RATE_LIMIT_EXCEEDED(429) — auth rate-limit window tripped.
Example
curl -X POST https://api.e-bon.ro/api/v1/auth/logout \
-H "Authorization: Bearer <portal-jwt>" \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "eyJhbGciOi..." }'
Token lifetimes and rate limits
| Token | Default TTL | Configured via |
|---|---|---|
| Access token | 15 min | JWT_ACCESS_EXPIRES_IN env var. |
| Refresh token | 7 days | JWT_REFRESH_EXPIRES_IN env var. |
Both tokens are signed JWTs (iss: e-bon-api, sub: <userId>). The access token uses JWT_SECRET; the refresh token uses a separate JWT_REFRESH_SECRET. Refresh tokens are also stored server-side as SHA-256 hashes so they can be revoked by /logout and rotated by /refresh.
| Limiter | Default max | Window | Key | Applies to |
|---|---|---|---|---|
/api/v1/auth/* | 30 | 10 min | Client IP | All five routes above. |
The auth limit is configured by AUTH_RATE_LIMIT_MAX and AUTH_RATE_LIMIT_WINDOW_MS. The same Retry-After and RateLimit-* headers documented on API overview › Rate limits apply.
Password reset flow
The /auth/forgot-password endpoint does not itself reset the password — it only triggers the email. The full round-trip is:
Client calls POST /api/v1/auth/forgot-password with the user's email
The API forwards the request to Firebase Identity Toolkit with requestType: PASSWORD_RESET. Firebase decides whether the address belongs to a real account; the API always responds 200 with the same generic message regardless.
User receives the email and clicks the reset link
The link points at the Firebase-hosted reset page (or a custom actionCodeSettings URL when configured in the Firebase console). The e-bon API is not in this hop.
Firebase verifies the action code and accepts a new password
Once the new password is saved, the user's refresh tokens stored in Firestore are not automatically revoked by Firebase. To force every existing session to log out, the user (or an Owner) should explicitly call POST /api/v1/auth/logout for each known refresh token, or wait for the 7-day refresh-token TTL to lapse.
User signs in again with POST /api/v1/auth/login
The new login issues a fresh access + refresh token pair and stores the refresh-token hash under the standard Firestore path.
See also
- Authentication — concept-level overview, API key format and the nine scopes.
- Users API — once logged in,
GET /api/v1/users/mereturns the authenticated profile andPOST /api/v1/users/me/change-passwordlets a signed-in user rotate their own password. - Organizations & Locations API — Portal-JWT routes for the org profile, billing address, locations and notification subscribers.
- API overview — base URL, error envelope, rate limits, idempotency, pagination, full HTTP error code catalogue.
- Errors reference — per-code recovery steps for every HTTP error code returned by the API.
Users
REST endpoints for the authenticated user's own profile — fetch, edit display name and phone number, change password.
Health, identity & meta endpoints
Reference for the public health probes, the authenticated identity introspection endpoint, the robots exclusion file and the OpenAPI surface (Swagger UI + raw spec) exposed by the e-bon API.