Skip to navigation

Authentication and sessions

JWT sessions with an idle timeout and a hard cap, rotating refresh, and long-lived API tokens

Every endpoint except registration, login, and the password and verification flows requires a bearer credential in the Authorization header. There are two kinds.

JWT sessions — for an app with a signed-in user

This section is the whole contract: a client written from this page alone is correct.

Signing in

POST /api/v1/auth/login with the user’s credentials and, optionally, whether to keep them signed in:

{
"email": "broker@example.com",
"password": "...",
"remember_me": true
}

It returns a token pair:

{
"email": "broker@example.com",
"tokens": {
"access": "eyJhbGciOi...",
"refresh": "eyJhbGciOi..."
}
}

Send the access token on every request:

curl -H "Authorization: Bearer <access_token>" \
https://api.finput.com.au/api/v1/billing/entitlements

When login is refused

A refused login answers 401 with a detail to show the user and a code to branch on:

{
"detail": "Unable to log in with the credentials provided.",
"code": "invalid_credentials"
}
codeWhenWhat the user can do
invalid_credentialsThe email is unknown, the password is wrong, or the account is locked after repeated failuresCheck the details, or reset the password
email_not_verifiedThe password is right, but the email has not been verified yetOpen the verification email, or ask for a new one with POST /api/v1/auth/resend-verification
account_deactivatedThe password is right, but an organisation admin has deactivated the accountAsk an admin of the organisation to reactivate it

Only a correct password gets email_not_verified or account_deactivated, so none of these codes tells a caller without the password whether an email is registered. Branch on code, not on the wording in detail.

How long a session lasts

A session is everything from one sign-in to the next. It ends at whichever of two limits comes first:

  • Idle timeout. Every refresh pushes the session’s expiry to one idle period from now, so a session in use stays signed in and one left alone ends.
  • Hard cap. However active it is, a session ends this long after sign-in. Refreshing never moves it: every rotated refresh token carries the original sign-in time.

remember_me at sign-in picks the pair, and it holds for the life of the session:

remember_meSigned out after inactivityHard cap from sign-in
false or omitted3 days14 days
true30 days90 days

These are the current defaults.

  • An access token lasts about an hour (current default), and never outlives its session: near the end of a session, the access token expires when the session does.
  • Don’t hardcode any of these numbers. They are server configuration. If you need to show when a session will end if left alone, read the exp claim of the latest refresh token; session_start is when the session signed in. A client that never reads a claim at all and simply follows the rules below is also correct.

Carrying the tokens

Login and refresh return both tokens in the JSON body. Refresh and logout take the refresh token in the body, and every request sends Authorization: Bearer <access>. No cookies or CSRF handling are involved.

Refreshing

When the API answers 401, exchange the refresh token at POST /api/v1/auth/refresh with {"refresh": "<refresh_token>"}. The response is a new pair, {"access": "...", "refresh": "..."}.

Every refresh rotates the refresh token: the one you sent is spent, and the response carries its successor. A spent token is not dead straight away. For a short grace window (currently 30 seconds) after it was spent, presenting it again returns the same successor the first refresh issued, not a new one. After the window it is refused with 401.

That window is what keeps a client simple. Concurrent refreshes converge, and a lost response can be retried.

The rules a client implements

  1. On a 401 from any endpoint other than refresh, refresh once, then retry the original request once with the new access token. If the retried request is also 401, or the refresh itself is 401, the session is over: discard both tokens and sign the user out.
  2. Store the refresh token from every refresh response, replacing the one you sent. Concurrent refreshes return the same refresh token, so it does not matter which response you store last.
  3. Concurrent requests need no coordination. Several requests that hit 401 together may each refresh with the same token; within the grace window they all receive the same successor and all succeed. Sharing one in-flight refresh (“single-flight”) is a sensible optimisation, but correctness does not depend on it.
  4. A refresh that fails without a response may be retried with the same token, promptly. If the server did rotate, the retry inside the grace window returns the same successor. Retry for a few seconds, not minutes. Once the window has passed, the answer is 401.
  5. 429 from refresh is not a sign-out. Refresh is rate limited per user, not per IP, so colleagues behind one office connection do not share a budget. Back off and try again. Only 401 ends a session.

Signing out

POST /api/v1/auth/logout with the access token as the bearer and {"refresh": "<refresh_token>"} in the body ends the session immediately. There is no grace window on logout. If the refresh token you hold was spent moments ago by a concurrent refresh, logout still ends the session: it revokes the successor too.

The access token stays valid until it expires, which is inherent to stateless JWT, so discard your own copy of both tokens whatever the response. A 400 means the refresh token was already unusable. The session is over either way.

Where to keep the tokens

  • Native apps: in the platform’s secure store (the iOS Keychain, or Android Keystore-backed encrypted storage). Never plain preferences or a file.
  • Browser apps: keep the access token in memory rather than localStorage, where an injected script could copy it.

API tokens — for scripting without a browser

A long-lived finput_sk_... token, minted at POST /api/v1/api-tokens, goes in the same header. The secret is shown exactly once, when the token is created.

A token’s scope is about effect, not HTTP method: read covers every GET plus the five computation endpoints, and read_write adds the endpoints that mutate. A token is always bounded by the intersection with its owner’s own permissions, and privilege-sensitive endpoints — passwords, user management, billing changes, token management, connection management — refuse token auth outright whatever the scope.

See the API Tokens endpoints for the full scope rules.