e-bon
e-bon.ro
API reference

Authentication endpoints

REST endpoints under /api/v1/auth — register, login, forgot password, refresh and logout — that issue and revoke the JWT access + refresh token pair used by the e-bon Portal and the E-BON mobile app.

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.

For a concept-level introduction to the two auth modes (API key vs Portal JWT), how to send the access token and how scopes work, see Authentication. The page you are on is the wire-level reference for the five /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

FieldTypeRequiredNotes
emailstringyesValid email address. Becomes the Firebase Auth login.
passwordstringyesMinimum 8 characters.
companyNamestringyes1–255 chars. Used as both the organization name and the Firebase Auth displayName.
cuistringyesRomanian fiscal code, max 20 chars. Must match ^(RO)?\d{2,10}$ (e.g. RO12345678 or 12345678).
addressstringnoUp 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, malformed cui, oversized field).
  • CONFLICT (409) — An account with this email already exists — Firebase Auth refused with auth/email-already-exists.
  • RATE_LIMIT_EXCEEDED (429) — auth rate-limit window tripped. Retry after the Retry-After seconds.
  • INTERNAL_ERROR (500) — Failed to create user account (unexpected Firebase Auth error) or Failed 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

FieldTypeRequiredNotes
emailstringyesValid email address.
passwordstringyesNon-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 misconfigured when the server is missing FIREBASE_WEB_API_KEY and 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

FieldTypeRequiredNotes
emailstringyesValid 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 malformed email).
  • RATE_LIMIT_EXCEEDED (429) — auth rate-limit window tripped.
If the API server has no 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

FieldTypeRequiredNotes
refreshTokenstringyesNon-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 empty refreshToken).
  • UNAUTHORIZED (401) — Invalid or expired refresh token (signature, issuer or TTL check failed) or Refresh token has been revoked (the hash is no longer in Firestore — typically because /logout deleted it or because a previous /refresh already 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

FieldTypeRequiredNotes
refreshTokenstringyesNon-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

TokenDefault TTLConfigured via
Access token15 minJWT_ACCESS_EXPIRES_IN env var.
Refresh token7 daysJWT_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.

LimiterDefault maxWindowKeyApplies to
/api/v1/auth/*3010 minClient IPAll 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.

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/me returns the authenticated profile and POST /api/v1/users/me/change-password lets 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.