Authentication
These routes create and end sessions. They live under /api/auth, not /api/v1, and all of them are anonymous. For how a session or an API key is then used on the rest of the API, see the REST API overview.
Routes
| Method and path | Role | Purpose |
|---|---|---|
POST /api/auth/sign-up/email | anonymous | Create the gateway's first account (its admin) and sign it in |
POST /api/auth/sign-in/email | anonymous | Sign in with email and password; sets the session cookie |
GET /api/auth/get-session | anonymous | The current session and user, or null |
POST /api/auth/sign-out | anonymous | End the session and clear the cookie |
GET /api/auth/idp | anonymous | The enabled identity providers, for the login page's buttons |
GET /api/auth/idp/{id}/start | anonymous | Redirect the browser to the provider |
GET /api/auth/idp/{id}/callback | anonymous | The provider's return address; signs the user in and redirects to / |
The session cookie
A successful sign-in sets the cookie scadex.session. It is HTTP-only, expires 7 days after sign-in (no sliding renewal), and is marked Secure when AUTH_BASE_URL starts with https://. The cookie carries an encrypted session id; the session row in the database is looked up on every request, so a deleted or expired session is refused even if the cookie is still present, and the role in the response is always the role currently stored for the user.
Requests that change state and carry the cookie must also send a trusted Origin header; see the Origin requirement. That includes POST /api/auth/sign-in/email once a cookie exists, and POST /api/auth/sign-out.
Sign up: the first account
POST /api/auth/sign-up/email
| Field | Type | Required | Meaning |
|---|---|---|---|
email | string | yes | Sign-in email; must be unique |
name | string | yes | Display name |
password | string | yes | Stored as a hash |
Public sign-up succeeds only while the gateway has no user at all. The account it creates gets the admin role and is signed in at once. The check and the insert share one database transaction, so two callers racing to claim a new gateway cannot both win. After the first account exists, every call answers 403 {"code":"FORBIDDEN","message":"Public sign-up is disabled"}; further users are created by an admin through POST /api/v1/admin/users (see Administration).
Response 200:
{ "token": "<session id>", "user": { "id": "...", "name": "...", "email": "...", "emailVerified": false, "image": null, "role": "admin", "createdAt": "2026-11-08T12:00:00+00:00", "updatedAt": "..." } }
Sign in
POST /api/auth/sign-in/email
| Field | Type | Required | Meaning |
|---|---|---|---|
email | string | yes | |
password | string | yes | |
rememberMe | boolean | no | Default true. false makes the cookie a session cookie that the browser drops when it closes; the 7-day expiry still applies |
callbackURL | string | no | Accepted for compatibility with the SDK client; the gateway does not act on it |
Response 200:
{ "redirect": false, "token": "<session id>", "url": null, "user": { "id": "...", "name": "...", "email": "...", "emailVerified": false, "image": null, "role": "operator", "createdAt": "...", "updatedAt": "..." } }
A wrong email or password answers 401 {"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"}. Only accounts with a password (credential accounts) can sign in here; an account created by an identity provider has none unless an admin set one.
The current session
GET /api/auth/get-session
Returns null (HTTP 200, body null) when the request carries no valid session. Otherwise:
{
"session": { "id": "...", "expiresAt": "2026-11-15T12:00:00+00:00", "token": "...", "createdAt": "...", "updatedAt": "...", "ipAddress": null, "userAgent": null, "userId": "..." },
"user": { "id": "...", "name": "...", "email": "...", "emailVerified": false, "image": null, "role": "viewer", "createdAt": "...", "updatedAt": "..." }
}
user.role is one of viewer, operator, engineer, admin. Timestamps are ISO-8601 strings. A request authenticated with an API key has no session row, so this route answers null for it.
Sign out
POST /api/auth/sign-out
Deletes the session row and clears the cookie. Always answers 200 {"success": true}, also when no session was present.
Signing in with an identity provider
Identity providers are OpenID Connect providers an admin configures (see Identity providers and the admin routes in Administration). The login flow is browser-driven:
GET /api/auth/idplists the enabled providers as[{ "id": "...", "name": "..." }], sorted by name.GET /api/auth/idp/{id}/startdiscovers the provider's configuration from its issuer, generates PKCE and state values, stores them in the cookiescadex.idp(HTTP-only,SameSite=Lax, path/api/auth/idp, 10 minutes) and redirects to the provider's authorization URL with the scopes configured for the provider. The redirect URI is{AUTH_BASE_URL}/api/auth/idp/{id}/callback.- The provider sends the browser to
GET /api/auth/idp/{id}/callback?code=&state=(or?error=). The state cookie is deleted on arrival, so each start allows one attempt. The gateway exchanges the code (with the client secret if one is stored), validates the ID token and its nonce, resolves the account, creates a session, setsscadex.sessionand redirects to/.
Who the sign-in becomes:
- A subject that has signed in through this provider before is recognized by the provider's own id for them, not by email.
- A new subject needs a verified email. If the provider has allowed domains, the email's domain must be one of them.
- If a user with that email already exists: the sign-in is linked to it only when the provider has Link existing accounts on and the user is not an admin. Otherwise it fails.
- Otherwise a new user is created with the provider's default role, capped at engineer (an unknown or
admindefault becomesviewer).
Failures do not return JSON. The callback redirects to /login?error=<code> with one of:
| Code | Meaning |
|---|---|
provider_unavailable | The provider is unknown, disabled, or its stored secret cannot be read |
provider_denied | The provider returned an error parameter |
invalid_state | Missing or expired state cookie, state mismatch, or no code |
email_not_verified | The provider did not mark the email as verified |
domain_not_allowed | The email's domain is not in the provider's allowed list |
account_exists | A user with that email exists and cannot be linked |
sign_in_failed | Any other refusal while resolving the account |
Other codes come from the OIDC client itself when discovery, the token exchange or the token validation fails.