Skip to main content

Authentication

The IdentSphere SDK accepts credentials in two forms. Both are equally first-class.

Issued automatically by every login endpoint. The browser sends them back on every same-origin request; you don't have to touch them in JavaScript.

Cookie: identsphere_at=eyJ...; identsphere_rt=def...; identsphere_csrf=abc...
CookieContentsRequired for
identsphere_atSigned JWT access tokenEvery authenticated request
identsphere_rtOpaque refresh tokenPOST /v1/auth/refresh, POST /v1/auth/logout
identsphere_csrfCSRF double-submit tokenEvery state-changing request from a browser

CSRF protection​

State-changing methods (POST, PATCH, DELETE) sent with cookie auth require the CSRF double-submit pattern:

  1. Read the identsphere_csrf cookie (it is intentionally NOT HttpOnly for exactly this reason).
  2. Send its value back in an X-IdentSphere-CSRF header on every state-changing request.
const csrf = document.cookie
.split('; ')
.find(c => c.startsWith('identsphere_csrf='))
?.split('=')[1];

fetch('https://auth.example.com/v1/users/me/password', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-IdentSphere-CSRF': csrf!,
},
body: JSON.stringify({ current_password: 'old', new_password: 'newpw_at_least_12_chars' }),
});

The header name is matched case-insensitively (x-identsphere-csrf works too). A mutating cookie-authed request with a missing or mismatched header is rejected with 403 and error code csrf_failed.

::: warning Why double-submit, not synchronizer pattern? The double-submit pattern is stateless on the server side and matches the SDK's "no per-process state" design. The CSRF cookie is bound to the same domain as the API, so a cross-origin attacker can't read it; the attacker can't construct a request that has a matching cookie value AND header. :::

The @identsphere/react client handles this for you automatically.

Bearer auth (services, mobile, CLI)​

Pass the access token in the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...

Bearer auth bypasses the CSRF requirement — there's no cookie to forge.

Use Bearer auth when:

  • You're a mobile app that just received access_token from /v1/auth/login
  • You're a server-side worker holding a long-lived token
  • You're hitting the API from a different origin without CORS credential support

Don't mix: a single request shouldn't carry BOTH a cookie and an Authorization header. The middleware prefers the header.

Refresh flow​

Access tokens expire fast (default 15 min). When they do, you don't have to log the user back in — call POST /v1/auth/refresh:

┌─────────┐ ┌─────────────────┐
│ Client │ │ IdentSphere backend │
└─────────┘ └─────────────────┘
│ GET /api/me │
│ Cookie: identsphere_at=... │
│ ─────────────────────────► │
│ │
│ 401 Unauthorized │
│ ◄───────────────────────── │
│ │
│ POST /v1/auth/refresh │
│ Cookie: identsphere_rt=... │
│ ─────────────────────────► │
│ │
│ 200 OK │
│ Set-Cookie: identsphere_at=... │
│ Set-Cookie: identsphere_rt=... │ ◄── rotated
│ Set-Cookie: identsphere_csrf=... │
│ ◄───────────────────────── │
│ │
│ GET /api/me │ ◄── retry
│ Cookie: identsphere_at=NEW │
│ ─────────────────────────► │
│ │
│ 200 OK │
│ ◄───────────────────────── │

Refresh tokens rotate on every use. The old token becomes invalid immediately — replaying a previous refresh token is a family-detection signal that revokes the entire session family.

@identsphere/react wraps this flow in an Axios interceptor; non-React clients should run a single retry on 401 and then surface the error.

MFA-required responses​

Login may return 200 OK with status: "mfa_required" instead of a full session. The client should:

  1. Capture the mfa_token (lifetime: 5 minutes, single-use).
  2. Prompt the user for a TOTP code OR a recovery code.
  3. POST /v1/auth/mfa/challenge with { mfa_token, code } or { mfa_token, recovery_code }.
  4. On success, the response is a normal status: "success" body — the user is now signed in.

See POST /v1/auth/mfa/challenge for the full shape.

No auth required​

A small set of endpoints accept anonymous traffic:

  • POST /v1/auth/register
  • POST /v1/auth/login
  • POST /v1/auth/refresh
  • POST /v1/auth/logout (degrades gracefully without auth — still clears cookies)
  • POST /v1/auth/email-otp/request
  • POST /v1/auth/email-otp/verify
  • POST /v1/auth/email/verify
  • POST /v1/auth/password/forgot
  • POST /v1/auth/password/reset
  • POST /v1/auth/passkey/login/begin
  • POST /v1/auth/passkey/login/complete
  • POST /v1/auth/mfa/challenge
  • GET /v1/auth/oauth/:provider/start
  • GET /v1/auth/oauth/:provider/callback
  • GET /v1/auth/invitations/preview
  • POST /v1/auth/invitations/accept (auth optional)