Skip to content

Sessions ​

Session endpoints manage authentication — login, mid-login challenge resume, token refresh, and logout.

Login ​

API ​

POST /gatelin/sessions
Content-Type: application/json

{
  "email": "user@example.com",
  "pwd": "password"
}

Content-Type must be application/json (415 otherwise). That blocks a cross-site HTML form from logging the browser into another account. The same rule applies to Resume.

Response (200 OK) — password accepted and no mid-login challenge required:

json
{
  "nickname": "jane",
  "accessToken": "eyJhbGc...",
  "roles": [1, 2],
  "permissions": [
    { "route": 4, "operations": [1, 2], "fields": [], "scopes": [] }
  ]
}

A CSRF cookie (csrfToken by default) is also set. When REFRESH_TOKEN_COOKIE is enabled, the refresh token is stored as an httpOnly cookie (and omitted from the JSON body when marked private).

Response (202 Accepted) — password accepted, but a mid-login challenge must be completed first:

json
{
  "challengeRequired": true,
  "kind": "2fa",
  "url": "https://example.com/api/pwd/web/2fa/verify?challenge=…"
}
kindWhen it is returned
expired-passwordThe account's password has expired
2faTwo-factor is enabled and the browser has no valid trusted-device cookie

The client must send the browser to url. That URL is served entirely by your password service — Gatelin does not render or control it. When the workflow finishes, your service redirects the browser back to the admin login with ?ticket=…, and the frontend calls Resume to create the session.

Other error statuses:

StatusMeaning
400Missing or invalid email / pwd
401Wrong credentials
403Account locked
404User not found

Password-service contract ​

Gatelin never stores password hashes or 2FA secrets, and never renders challenge pages. It delegates all of that to a password service you point PWD_CHECK_URL at. Any service that speaks the small HTTP contract below works.

Credential check (required) ​

PWD_CHECK_URL is the only endpoint Gatelin always calls. On a correct password it must return HTTP 200 with a single user row:

json
{
  "rows": [{
    "userId": 42,
    "pwdExpiry": null,
    "lockedUntil": null,
    "twoFactorEnabled": false
  }],
  "total": 1
}

On a wrong password it must return a non-2xx status (typically 401).

Gatelin reads only these fields from rows[0]; any others are ignored:

FieldTypeEffect on login
lockedUntilISO date string or nullIf in the future, login is rejected with 403
pwdExpiryISO date string or nullIf in the past, login returns 202 expired-password
twoFactorEnabledbooleanIf true (and no trusted-device cookie), login returns 202 2fa

Don't need challenges? If your service only checks passwords, return a body without a rows entry (for example { "success": true }, or { "rows": [], "total": 0 }). Gatelin then treats the login as fully authenticated, skips all gating, and logs a debug/warn note. This is the correct setup for a plain username+password login with no 2FA or password-expiry policy.

Challenge endpoints (only if you emit 202) ​

If your credential check can return pwdExpiry, lockedUntil, or twoFactorEnabled, then Gatelin also needs the endpoints below. Each one has its own environment variable — nothing is derived from PWD_CHECK_URL, so you are free to name and place these routes however you like:

VariableRequest bodyExpected responseCalled when
PWD_CHALLENGES_URL{ userId, kind }{ url, kind } — url is the browser page that runs the challengeA challenge is required
PWD_TRUSTED_DEVICES_URL{ userId, deviceToken }{ trusted: boolean }2FA is enabled and a trusted-device cookie is present
PWD_LOGIN_TICKET_URL{ ticket }{ userId }The frontend calls Resume

All three are POST endpoints and all three may be empty. Gatelin still boots and password login still succeeds. Empty PWD_CHALLENGES_URL skips 2FA and password-expiry pages (a warn is logged). Empty PWD_TRUSTED_DEVICES_URL ignores the trusted-device cookie. Empty PWD_LOGIN_TICKET_URL makes Resume answer 501.

To let a returning browser skip 2FA, your challenge pages may set a trusted-device cookie named trusted_device (scoped Path=/ so it reaches /gatelin/sessions). On the next login Gatelin forwards its value to PWD_TRUSTED_DEVICES_URL; a { trusted: true } response suppresses the 2FA challenge. If you don't implement trusted devices, simply never set the cookie — every 2FA login then challenges.

After a password reset or change ​

Gatelin does not learn that a password changed. PUT /gatelin/sessions only refreshes tokens for a still-live consumer; the user row stays active, so a stolen refresh token keeps working until that consumer is archived.

The password service must therefore kill sessions with the existing consumer APIs, using a system account whose roles include getConsumers and archiveConsumers (Admin already has both). If that account cannot archive, do not reset the password — stop with an error.

  1. Confirm the account can archive (a 403 on the next step is a hard failure).
  2. Search live sessions for the user:
http
POST /gatelin/consumers/search
Authorization: Bearer <system-account-access-token>
Content-Type: application/json

{
  "pagination": false,
  "filters": {
    "userId": { "value": 42, "matchMode": "equals" },
    "archived": { "value": false, "matchMode": "equals" }
  }
}
  1. Commit the password change.
  2. Archive every returned id:
http
POST /gatelin/consumers/archive
Authorization: Bearer <system-account-access-token>
Content-Type: application/json

{ "rows": [{ "id": 10 }, { "id": 11 }] }

If search returns no rows, there is nothing to kill. If archive fails after the password already changed, retry until it succeeds — otherwise stolen refresh tokens remain valid. Access tokens already minted still work until they expire.

Sequence diagram ​

Resume ​

Finishes a login that was interrupted by a mid-login challenge. Public — no JWT required.

POST /gatelin/sessions/resume
Content-Type: application/json

{
  "ticket": "one-shot-login-resume-ticket"
}

Gatelin redeems the ticket against the password service (POST PWD_LOGIN_TICKET_URL), looks the user up by id, then runs the same token / session / CSRF path as a successful login.

Response (200 OK): same session payload as Login.

StatusMeaning
400Missing, invalid, or already-consumed ticket
422Ticket redeemed but the user lookup failed

Tickets are one-shot and short-lived. The admin UI reads ?ticket= on the login page and calls this endpoint automatically. Content-Type must be application/json (415 otherwise), same as Login.

Refresh Tokens ​

PUT /gatelin/sessions
Content-Type: application/json
X-CSRF-Token: <csrf_cookie_value>
Cookie: csrfToken=<csrf_cookie_value>; refreshToken=<httpOnly>

{}

The access token may already be expired — refresh ignores expiration after CSRF and refresh-token checks pass. Browsers should send an empty body and rely on the httpOnly refresh cookie (credentials: include). Non-browser clients may put refreshToken in the JSON body instead of a cookie.

Response (200 OK):

json
{
  "nickname": "jane",
  "accessToken": "new_access_token",
  "roles": [1, 2],
  "permissions": [
    { "route": 4, "operations": [1, 2], "fields": [], "scopes": [] }
  ]
}

The refresh token is isPrivate and is not returned in JSON. A fresh CSRF cookie and (when enabled) refresh cookie are set on the response.

Logout ​

DELETE /gatelin/sessions
Authorization: Bearer <access_token>
X-CSRF-Token: <csrf_cookie_value>
Cookie: csrfToken=<csrf_cookie_value>

Response (204 No Content)

The consumer session is archived, removed from the cache, and refresh/CSRF cookies are cleared.

Published and maintained by ALTEN