Authentication
Every route requires a session, established one of three ways: OIDC (the production path — bring your own identity provider), a break-glass password login for the one account defined directly in the environment, or an API key for a script or service. This page assumes you already know what OIDC is; if you don’t, your identity provider’s own docs are the right place to start, not here.
flowchart LR
oidc["OIDC login"] --> cookie
bootstrap["Break-glass password"] --> cookie
apikey["API key\n(Bearer token)"] -.->|"no cookie —\nchecked per request"| ok
cookie["Session cookie\n(signed JWT)"] --> ok["Authenticated request"]
Whichever path is used, the result funnels down to the same thing: a
signed, stateless JWT in an HttpOnly cookie, or a Bearer token checked on
each request for API keys. GET /auth/me reads the cookie back;
POST /auth/logout clears it. The frontend calls GET /auth/config to
know whether OIDC is even set up — an unconfigured discovery URL hides the
login button entirely rather than showing one that would just error.
No session table, on purpose
Section titled “No session table, on purpose”Logging in never writes a row, and checking a session never round-trips to Postgres — the cookie is verified by signature alone. The cost is revocation: there’s no “log this one session out” lever, only the signing secret itself, rotating which invalidates every session at once. For a deployment where the identity provider is the real source of truth on who’s allowed in, that trade reads as the right one: the rare “kick everyone out now” case is handled, and the common case pays no database cost at all.
Two different password hashes, for two different threat models
Section titled “Two different password hashes, for two different threat models”A principal’s secret — a bootstrap password or an API key — is stored as one of two different hash formats, picked by which kind of secret it is:
- A human password is hashed with bcrypt: salted and deliberately slow, because a person-chosen password is comparatively low-entropy and has to resist offline brute-force guessing.
- An API key is hashed with a plain SHA-256 digest: unsalted and fast, because the key itself is a 32-byte random token — high-entropy enough that brute force isn’t the risk — and a plain digest is what makes an indexed lookup possible when a Bearer token arrives with no username attached to tell Atlas which principal to check it against first.
Claims, and what Atlas does with them
Section titled “Claims, and what Atlas does with them”Only two OIDC claims are singled out by name — everything else your provider returns (email, groups, department, whatever) is stored as-is on the principal and available to authorization’s matching rules with no extra setup:
ATLAS_AUTH_OIDC_ID_CLAIM=sub # becomes the principal's idATLAS_AUTH_OIDC_NAME_CLAIM=name # becomes its display nameChanging ATLAS_AUTH_OIDC_ID_CLAIM after real users already exist orphans
every principal created under the old claim — treat it as fixed once you’ve
gone live.
Configuring OIDC
Section titled “Configuring OIDC”Minimum to turn the login button on:
ATLAS_AUTH_OIDC_DISCOVERY_URL=https://idp.example.com/realms/atlas/.well-known/openid-configurationATLAS_AUTH_OIDC_CLIENT_ID=atlasATLAS_AUTH_OIDC_CLIENT_SECRET=...A fuller configuration — matching roles against an IdP-managed groups
claim, and being explicit about claim mapping and PKCE rather than relying
on the defaults:
ATLAS_AUTH_OIDC_DISCOVERY_URL=https://idp.example.com/realms/atlas/.well-known/openid-configurationATLAS_AUTH_OIDC_CLIENT_ID=atlasATLAS_AUTH_OIDC_CLIENT_SECRET=...ATLAS_AUTH_OIDC_ID_CLAIM=preferred_usernameATLAS_AUTH_OIDC_NAME_CLAIM=nameATLAS_AUTH_OIDC_SCOPE=openid profile email groupsATLAS_AUTH_OIDC_ENABLE_PKCE=trueATLAS_ALLOW_SELF_REGISTRATION=falseATLAS_AUTH_FRONTEND_URL=https://atlas.example.comATLAS_ALLOW_SELF_REGISTRATION=false means an admin must create a
principal (or a role’s grant_match must already cover them, e.g. by
group) before that identity can log in at all; true auto-provisions on
first login instead. See CORS & Authentication
for the full variable reference.
Calling the API
Section titled “Calling the API”curl -c cookies.txt -X POST http://localhost:8000/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"username": "admin", "password": "changeme"}'
curl -b cookies.txt http://localhost:8000/api/v1/workspacescurl -H "Authorization: Bearer atlas_..." \ http://localhost:8000/api/v1/workspacesfrom atlas_client import AuthenticatedClientfrom atlas_client.api.workspaces import workspaces_list_all
client = AuthenticatedClient(base_url="http://localhost:8000", token="atlas_...")workspaces = workspaces_list_all.sync(client=client)