Auth

Sessions & token refresh

How long tokens live, how to refresh them safely, and how sessions end.

Every sign-in creates a session. The session holds a short-lived access token that you send with each request, and a refresh token that you exchange for new tokens. This page covers what your frontend has to handle so users stay signed in and a stolen token stops working.

Lifetimes

LifetimeWhat happens then
Access token15 minutesRequests answer AUTH_TOKEN_EXPIRED. Refresh and retry.
Refresh token (idle)30 days since the last refreshThe session ends. Each refresh restarts this window.
Session (absolute)90 days since sign-inThe session ends however active it is. The user signs in again.

Signing out takes effect immediately and doesn't wait for a token to expire. Every request checks that its session is still live.

Refresh tokens are single-use

Exchanging a refresh token gives you a new access token and a new refresh token. The one you sent stops working, so always store both tokens from the response.

curl -X POST "$AUTHORIC_URL/$APP_UID/auth/token" \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "<refresh token>" }'

Sending a refresh token that has already been exchanged ends the whole session with SESSION_ENDED and reason reused. A spent token that comes back usually means someone else copied it, and there's no way to tell the thief from the owner, so neither keeps the session.

The narrow exception

For 30 seconds after an exchange, the refresh token it replaced still returns the current pair instead of ending the session. This covers two browser tabs refreshing at the same moment, or a retry whose first response was lost. It's a safety net, so coordinate your refreshes anyway (see below).

The refresh request doesn't need an Authorization header, and any header you send is ignored. The access token it would carry is usually the expired one you're replacing.

Errors to branch on

Read error.code, not the message.

CodeStatusWhat to do
AUTH_TOKEN_EXPIRED401Refresh, then retry the request once.
AUTH_TOKEN_INVALID401Refresh, then retry the request once.
SESSION_ENDED401Don't refresh. Sign the user out and show the sign-in screen. error.details.reason says why: expired, revoked, reused, signed_out, invalid or legacy.

Any other 401 isn't about the user's session. For example, an external service might reject a key your app supplied. Show it as an ordinary error, and don't sign anyone out over it.

A refresh strategy that holds up

  1. Refresh early. Read the exp claim from the access token and refresh about a minute before it passes. Use exp for timing only, never for trust.
  2. Refresh on AUTH_TOKEN_EXPIRED / AUTH_TOKEN_INVALID as well, and retry the refused request once.
  3. Run one refresh at a time, across every tab of your app. Because refresh tokens are single-use, two tabs exchanging the same token at once is a replay.
  4. Keep the session on network errors. Being offline, a timeout or a 5xx doesn't mean the session is over. Back off and retry. Only SESSION_ENDED ends it.
  5. Store the pair you got back before doing anything else with it.

In a browser, a Web Lock handles point 3. Inside the lock, check whether another tab already refreshed before you spend your token:

async function refreshSession(staleAccessToken: string) {
  return navigator.locks.request("my-app-auth-refresh", async () => {
    const current = loadTokens();

    // Another tab refreshed while we waited: use its pair, spend nothing.
    if (current.accessToken !== staleAccessToken) return current;

    const response = await fetch(`${API}/${APP}/auth/token`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken: current.refreshToken }),
    });

    if (response.status === 401) {
      clearTokens();
      throw new Error("session ended"); // send the user to sign in
    }
    if (!response.ok) throw new Error("refresh failed"); // keep the session, retry later

    const next = await response.json();
    saveTokens(next); // other tabs see this through the `storage` event
    return next;
  });
}

If your tabs share tokens through localStorage, listen for the storage event so every tab picks up a new pair, or a sign-out, as soon as one tab writes it.

Where to keep the tokens

  • A server component of your own (recommended when you have one). It holds the refresh token in an httpOnly, Secure, SameSite cookie and refreshes on the browser's behalf. The browser only ever holds the short-lived access token, in memory.
  • Browser only. Keep the access token in memory and the refresh token in storage. Pair this with a strict Content Security Policy, because script injection anywhere in your app can read storage.

Never put the refresh token in a URL, and never log it.

Signing out

Sign-out ends the session everywhere it's used: its refresh token and every access token issued from it stop working. Send the refresh token in the body. Unlike the access token, it still works after the access token has expired.

curl -X DELETE "$AUTHORIC_URL/$APP_UID/auth/logout" \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "<refresh token>" }'

Signing out is idempotent: ending a session that has already ended still returns 200.

Where a user is signed in

A signed-in user can list their own sessions and end any of the others. The session making the request is marked current. To end that one, sign out instead.

# List
curl "$AUTHORIC_URL/$APP_UID/auth/sessions?limit=25&offset=0" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Sign out one other session
curl -X DELETE "$AUTHORIC_URL/$APP_UID/auth/sessions/<session uid>" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Sign out everywhere else
curl -X DELETE "$AUTHORIC_URL/$APP_UID/auth/sessions" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Each entry has a coarse label (such as Chrome on macOS), how the session began, its IP address, and when it was last used. These routes are part of the Manage my own account capability in your policies.

When sessions end without the user signing out

  • Password reset ends every session the account has.
  • Turning on two-factor authentication (confirming a factor) ends every other session. So does removing the last factor.
  • A replayed refresh token ends that session (reused).
  • Deleting the account ends its sessions on their next request.

Browsers remembered for two-factor authentication

A user who ticks "remember this device" when entering a code skips the code on that browser until the device expires. Signing out on that browser doesn't change this, and they still need their password (or another sign-in method) every time. The actions a user takes when they suspect someone else has their password do forget remembered browsers:

  • Signing out one session from the session list forgets the browser that session used.
  • Signing out everywhere else forgets every remembered browser except the current one.
  • Resetting the password forgets all of them.

Switching organizations

A user who belongs to several organizations switches by refreshing with activeOrgUid. The server checks membership before issuing tokens for the new organization. A switch is a refresh, so it spends the refresh token like any other and belongs inside the same lock.

{ "refreshToken": "<refresh token>", "activeOrgUid": "<organization uid>" }

Realtime subscriptions

A subscription stream is authenticated once, when it opens. When the access token it opened with expires, the server sends a reauth event and closes the stream. Reconnect with your current access token (refreshing first if needed) and pass the last event id to resume. See Realtime.

Don't verify tokens yourself

Access tokens are signed with a key only Authoric holds, and there's no public key to check them against. Treat them as opaque strings: send them, and read exp for timing. Authoric verifies every request. Your backend should forward the token and act on the response, not decode it and trust its contents.

When Authoric rotates its signing keys, tokens signed with the previous key keep working until they expire, so users notice nothing. If a key ever has to be withdrawn immediately, requests answer AUTH_TOKEN_INVALID, the refresh answers SESSION_ENDED, and users sign in again. A client that follows the rules above handles both cases without special code.

Next

On this page