OIDC provider¶
The platform can act as its own OpenID Connect issuer, letting relying
party (RP) apps sign in through it. The implementation is plain Go
(core/business/usecases/oidc) — no third-party identity server.
When does it turn on?
The provider surface is mounted only when OAUTH_ISSUER is set. Without it
the platform remains purely a client of Gerege SSO — see
Connect an app.
Endpoints¶
These sit at the root, outside the /api/v1 group, because their paths are
fixed by the OIDC specifications.
| Path | Method | What |
|---|---|---|
/.well-known/openid-configuration |
GET | Discovery document |
/.well-known/jwks.json |
GET | RS256 public keys |
/oauth2/auth |
GET | Authorization (code flow only) |
/oauth2/token |
POST | Token exchange |
/oauth2/introspect |
POST | RFC 7662 |
/oauth2/revoke |
POST | RFC 7009 |
/oauth2/sessions/logout |
GET | RP-initiated logout |
/userinfo |
GET · POST | User claims |
All are public — client authentication happens inside each endpoint per the protocol.
Supported capabilities¶
| Field | Value | Note |
|---|---|---|
response_types_supported |
code |
Authorization code flow only |
response_modes_supported |
query |
|
grant_types_supported |
authorization_code · refresh_token · client_credentials |
|
code_challenge_methods_supported |
S256 only |
plain defeats the point of PKCE (RFC 9700 §2.1.1) |
id_token_signing_alg_values_supported |
RS256 |
|
subject_types_supported |
public |
Pairwise is not implemented |
token_endpoint_auth_methods_supported |
client_secret_basic · client_secret_post · none |
none = public client (PKCE) |
scopes_supported |
openid · offline_access · profile · email · nationalid |
A static list |
Why is the scope list static?
Advertising the union of registered client scopes would expose internal
gateway service names (svc:*) publicly. Discovery therefore lists only the
standard scopes.
Deliberately unsupported: request / request_uri (JAR) — unused and it
widens the SSRF surface.
Claims¶
sub · iss · aud · exp · iat · auth_time · nonce
name · given_name · family_name · given_name_en · family_name_en
email · email_verified · national_id · register_number
google_sub · google_email · google_name · google_picture
sub is the platform's stable, opaque per-citizen identifier (a user UUID),
not the national ID. national_id / register_number are only included when
the nationalid scope is granted.
Login / consent flow¶
The provider drives login and consent through its own UI: Next.js pages
(/oauth/login, /oauth/consent, /oauth/logout) calling the provider
usecase's challenge API.
sequenceDiagram
participant RP as RP app
participant P as Gerege App (issuer)
participant U as Citizen
RP->>P: GET /oauth2/auth?client_id&redirect_uri&code_challenge
P-->>U: /oauth/login (login_challenge)
U->>P: sign in with eID
P->>P: POST /v1/provider/login/accept
P-->>U: /oauth/consent (consent_challenge)
U->>P: approve
P->>P: POST /v1/provider/consent/accept
P-->>RP: redirect_uri?code&state
RP->>P: POST /oauth2/token (code + code_verifier)
P-->>RP: access_token · id_token · refresh_token
| Endpoint | What |
|---|---|
GET /v1/provider/login |
Login challenge details |
POST /v1/provider/login/accept · /reject |
Decision |
GET /v1/provider/consent |
Consent challenge — which scopes are requested |
POST /v1/provider/consent/accept · /reject |
Decision |
POST /v1/provider/logout/accept |
Logout confirmation |
First-party clients: those listed in SSO_FIRSTPARTY_CLIENTS skip the
consent screen.
Flow state lives in oauth_challenges, HMAC'd with SSO_STATE_KEY (≥32 bytes).
Client registration¶
Two surfaces, both backed by the same oauth_clients store:
| Endpoint | Permission | What |
|---|---|---|
GET/POST /v1/applications |
gateway.manage |
List / create |
GET/PUT/DELETE /v1/applications/{id} |
gateway.manage |
Read / edit / delete |
POST /v1/applications/{id}/rotate-secret |
gateway.manage |
Rotate the secret |
PUT /v1/applications/{id}/secret |
gateway.manage |
Set the secret |
PUT /v1/applications/{id}/services |
gateway.manage |
Grant gateway services |
UI: Admin → Applications.
Under /admin/api/v1/..., authenticated with an admin API key — the
familiar management-API pattern (Stripe/Auth0). Use
Authorization: Bearer gsk_… or X-API-Key.
This surface must not be public
In production the edge nginx routes /admin to the web BFF; the admin
API is reachable only from the host's loopback.
Unsupported fields (present in Hydra but with no column here): backchannel /
frontchannel logout URIs, DPoP, jwks/jwks_uri, audience,
sector_identifier_uri, pairwise subject_type. Requests carrying them are
rejected with 400 rather than silently dropped — so an operator never
believes they configured something that is in fact inert.
Key management¶
KeyManager generates the RS256 key and publishes it through JWKS. On rotation
the previous public key stays in JWKS for a while so in-flight id_tokens still
verify.
Token storage¶
| Table | What |
|---|---|
oauth_auth_codes |
Single-use authorization codes (with the PKCE challenge) |
oauth_access_tokens |
Issued tokens (for introspection / revocation) |
oauth_consents |
Consent granted by the user |
oauth_challenges |
Transient login / consent state |
All are protected by RLS — service and admin policies, plus a self policy
on oauth_consents.