Authentication model
Tokens
Keycloak issues OAuth 2.0 access tokens, signed JWTs, from the realm startupos. The frontend asks for them. Services only validate them.
| Token | Issued by | Carried how | Lifetime |
|---|---|---|---|
| Access token | Keycloak, through the frontend's /api/auth/token and /api/auth/refresh | Authorization: Bearer header | 30 minutes |
| Refresh token | Keycloak | HttpOnly cookie startupos_session, path /api/auth | Follows the realm's refresh settings |
| Stream token | LMS service | token query parameter on video and recording stream URLs | 6 hours |
| Service token | Keycloak, client credentials | Bearer header on service-to-service calls | Keycloak default |
| LiveKit webhook JWT | LiveKit server | Authorization header with no Bearer prefix | Per webhook |
How people sign in today
The pilot uses a demonstration sign-in. A person picks one of a fixed set of demo personas on the sign-in page and enters a password. The frontend's server route exchanges them with Keycloak for a token. Per-person accounts with their own credentials are not yet in place. The persona allow-list in frontend/src/lib/dev-personas.ts is what limits the route to those accounts.
How services decide who is calling
- The OIDC layer verifies the token and builds the security identity.
CallerIdentityandRoleEnforcementFilterread the principal and roles from that identity.- Header values are consulted only if
HeaderIdentityPolicyis enabled, which is true only in thedevandtestprofiles or when explicitly switched on. Production Deployments do not set it, and a startup guard refuses to start a production service in which it is on.
Roles and tenants
A Spoke Coordinator's token must carry a tenant claim that matches the tenant of the data requested. Platform-wide roles (hub_admin, govt_viewer) are exempt from the tenant match. The exemption is about scope, not permission: a CM Office user still needs the role a given endpoint requires.
Service-to-service
Services call each other with a client-credentials token. Roles are not borrowed: a service token carries none of the platform roles and cannot pass a @RequiresRole gate on a user's behalf.
Sources: frontend/src/app/api/auth/**, libs/common-models/src/main/java/com/rtih/startupos/common/security/, .kiro/steering/auth-rollout.md, docs/runbooks/keycloak-fix-final-report.md.