Signing in¶
SSO only, through Authentik at sso.pedshub.com — "One Sign In".
There is no sign-up form, no invite code and no email sign-in code. All three were this application doing the identity provider's job, and all three were removed; a migration drops their tables.
Every password door is closed. POST /auth/login answers 410 Gone, and
/register in the browser redirects to /login. The address is kept so an old
client is told plainly rather than getting a 404.
The flow¶
GET /api/v1/auth/sso/login 302 to the provider
…the person signs in there…
GET /api/v1/auth/sso/callback match on the email claim, apply the role,
park the token in Redis under a one-time
code, redirect with ?code=
POST /api/v1/auth/sso/exchange spend the code (GETDEL), return the token
Three details that are load-bearing:
sso/loginmust stayasyncand the redirect must be awaited. authlib's Starlette client returns a coroutine, and returning it unawaited is a 500 on every click.- The callback refuses an explicit
email_verified: false. - The access token never travels in a URL. It is parked in Redis under
sso:exchange:<code>for 60 seconds and exchanged in a POST body. nginx logs the request line, so a token in a query string is written to disk on every sign-in.
Accounts are created on first sign-in, matched by email address. A different address is a different account.
Roles from groups¶
With OIDC_ROLE_CLAIM set, services/sso_roles.apply() runs at every SSO
sign-in and brings the role into line with the person's groups — so removing
somebody from a group at the provider takes the role away here at their next
sign-in.
pedshub-admins and pedshub-moderators are the groups on this deployment.
Admin wins over moderator. The one demotion the sync refuses is the last
administrator.
While the mapping is on, the two in-app role endpoints answer 409 — see Access.
Staying signed in¶
An access token is a signed statement good for a day. Nothing consults a table before believing it, so it cannot be withdrawn — fine for a browser, which can send the person back to a sign-in page, and no use to an app, which would have to keep a credential to survive the night.
So a client that asks for one gets a refresh token:
POST /api/v1/auth/sso/exchange
{ "code": "<one-time code>", "refresh": true, "device": "PedsHub for iPhone" }
| Endpoint | Does |
|---|---|
POST /api/v1/auth/refresh |
Trades a refresh token for a new pair. The old one is spent. |
POST /api/v1/auth/logout |
Ends this session, or all of them with everywhere: true. |
GET /api/v1/auth/sessions |
Where this account is signed in — one row per device, not per rotation. |
DELETE /api/v1/auth/sessions/{family} |
Ends one of them. |
Three properties:
- Rotation. Every refresh issues a new token and spends the old one.
- A spent token coming back ends the whole session. Either it was copied or a client is replaying, and from the server those look identical — so the safe reading is the unsafe one. A thief who spends a token first makes the real client's next attempt fail, which is the theft announcing itself.
- Only the hash is stored. A database that leaks does not hand over live sessions with it.
A browser is not given one. It has nowhere safe to put it and a person sitting in front of it to ask again.
Logging out ends the ability to get another token. An access token already issued is a signature and cannot be recalled; it dies of old age within the day.
Configuring a provider¶
The redirect URI is always {APP_URL}/api/auth/sso/callback.
Applications → Providers → Create OAuth2/OpenID Provider, then an Application linked to it.
Cloud Console → APIs & Services → Credentials → OAuth client ID → Web application.
You may need to configure the OAuth consent screen first.
Azure Portal → App registrations → New registration. Redirect URI type Web. Take the client secret's value, not its id.
Use common for personal Microsoft accounts. Under API permissions,
ensure openid, email and profile are granted. For roles, set
OIDC_ROLE_CLAIM=roles.
Clients → Create client, OpenID Connect. The secret is on the Credentials tab.
After changing any of these, restart — they are environment, not code:
docker compose restart backend celery celery-beat.
When sign-in breaks¶
"SSO login failed" after the redirect.
APP_URLmust match the actual domain, withhttps.- The provider's redirect URI must match
{APP_URL}/api/auth/sso/callbackexactly. docker compose logs backend --tail=30 | grep -i "sso\|oidc\|oauth"
The name is wrong on a new account. The name claim is read first, then
preferred_username, then the email prefix. Some providers only include a name
when the profile scope is requested.
The provider is down, or you are locked out of it.
Sessions already open keep working — nothing here consults the provider after sign-in. Fix the provider rather than the application. Authentik's own break-glass is a one-time recovery link for its own admin, minted from the host:
Open the printed link within ten minutes to land in the Authentik admin interface without a password, then repair whatever broke — the flow, the outpost or the password.
If the provider is down entirely, docker compose up -d in
/home/danvics/docker/authentik-pedshub is the fix, not a side door here.
A second credential nobody rotates is a hole, not a spare key. That is why the
last password login was removed rather than kept for administrators.