Components
Identity and access
C4 level 3 for identity: how a token is verified without calling the identity provider, the four access lists that decide who may do what, and how deleting an account erases everything else first.
Suggest an editIdentity and access
You'll be able to: describe how the origin verifies a sign-in without a network call; name the four lists that grant access and what each one gates; and explain why an identity-provider outage returns 503 and not 401.
Signing in
Sign-in is OpenID Connect against Keycloak, the realm synapse, using the authorization-code flow
with PKCE. The browser does that part with keycloak-js, loaded only when a reader signs in. Keycloak
offers GitHub as its upstream provider, so Synapse never sees a password. What reaches the origin is a
bearer token on each request that needs one.
Verifying a token is local cryptography
JwksTokenVerifier (identity/infrastructure/jwks.rs) checks every bearer token without calling
Keycloak:
- RS256 against the realm's signing keys. The key set is fetched lazily, cached for about five minutes, and refreshed once when a token names an unknown key id — which absorbs key rotation.
- The issuer must match exactly;
expis checked with 60 s of leeway;subandexpare required. - The audience: Keycloak's public clients carry
aud: ["account"]and put the client inazp, so the rule isaudcontainssynapse-web, orazpissynapse-web. - The username leaves the verifier in canonical lowercase, applied once. Every later comparison — the admin check, every allowlist — is case-consistent by construction rather than by each caller remembering.
Two answers come out, and they are different on purpose. A bad token is InvalidToken → 401. A
key set that cannot be fetched is VerifierUnavailable → 503. "I cannot check" is not "you are
not allowed", and conflating them would lock out legitimate users during an outage. Reading and
running never verify a token at all, so a Keycloak outage does not stop anyone reading.
Other contexts call the identity application service and state their own 401 and 503 mapping. They never reach into the identity context's HTTP adapter.
Four lists decide access
Signing in proves who you are. These four lists decide what you may do:
| List | Stored in | Gates | Managed from |
|---|---|---|---|
| Admins | ADMIN_USERS env var (prod: ani2fun) |
every /api/admin/* route and the /admin page |
the deployment manifest — granting admin needs a commit |
| Submit allowlist | submission_allowlist |
judged submissions, when SUBMISSION_ALLOWLIST_ENFORCED is on (it is in prod) |
/admin |
| Content editors | content_editor_allowlist |
Suggest an edit (/api/edits) |
/admin |
| Private-book readers | content_source_reader |
a private source's lessons, media and widgets | /admin, per source |
Each list takes a canonical name of the same type the verifier produces, so a grant and the gate that checks it cannot disagree about case. The asymmetry in the first row is deliberate: everything else can be granted from a screen in a second, but making someone an admin takes a reviewed change to the deployment. Trade-offs argues it.
Deleting an account
/account offers four permanent actions behind confirm dialogs: reset progress, erase submissions,
erase all data, and delete the account. Deleting the account runs, in the browser:
- erase submissions,
- erase Think canvases,
- erase notes,
- reset progress,
DELETE /api/me— the origin deletes the Keycloak user,- sign out.
The orchestration sits on the client so that the identity context never depends on submissions, notes or progress. Lesson views need no step, because they never recorded who read them.
The Keycloak call uses KeycloakAdminClient, which signs in as a service account in Synapse's own
realm (synapse-admin, client credentials) holding only realm-management:manage-users. It is
never the master-realm administrator, whose leaked credentials would mean a full identity-provider
takeover. A delete is two calls — get a token, then DELETE /admin/realms/synapse/users/{sub} — and
both 204 and 404 count as success, because a user who is already gone is the outcome that was asked
for.