Skip to main content

Authentication

Services accept a Keycloak access token from the realm startupos, sent as Authorization: Bearer <token>. This guide covers how a token is obtained in the current pilot, how it is used, what 401 and 403 mean, and how it is renewed.

:::caution Do not paste real credentials into examples The examples use placeholders. This site has no "try it" panel for that reason, and the specs carry no server URLs. :::

Get a token​

In the pilot, tokens come from the frontend's own route, which signs in a demo persona against Keycloak.

curl -sS -X POST "https://<web-host>/api/auth/token" \
-H "Content-Type: application/json" \
-d '{"userId": "<persona id>", "password": "<password>"}'
  • userId must be one of the demo persona ids offered on the sign-in page. Any other value returns 400.
  • A missing password returns 400. A refused password returns 401. An unreachable identity provider returns 502. A deployment without sign-in configured returns 503.
  • On success the body is {"token": "<access token>", "expiresIn": <seconds>} and the response sets the HttpOnly cookie startupos_session with the refresh token.

Use the web host (rtih.peoplewave.in). The auth routes are served by the frontend, which is the catch-all only on the web hosts. On the API host the catch-all is the LMS service, so /api/auth/* there is not the frontend.

There is no other way to get a token for a person today. Keycloak is not reachable from outside the cluster. A public sign-in for the mobile apps is planned. See Mobile apps.

Use the token​

Send it on every call to /api/v1/*:

curl -sS "https://<web-host>/api/v1/lms/courses" \
-H "Authorization: Bearer <access token>"

The access token lasts 30 minutes. The services read the caller from the token's preferred_username claim and the roles from its realm roles. Do not send X-User-Id, X-User-Role or X-Tenant-Id as an identity. Production services ignore them.

401 and 403​

StatusMeaningWhat to do
401No token, an invalid token, or an expired token. The response carries WWW-Authenticate: Bearer.Renew the token or sign in again.
403The token is valid but the caller may not do this. The role is wrong, the tenant does not match, the caller is not enrolled, or the caller is not a moderator.Do not retry with the same token.

A role failure from RoleEnforcementFilter returns JSON with error: "FORBIDDEN", a message and the requiredRoles. See Errors for the other shapes.

Some operations do not use a bearer token:

  • Video and recording stream operations take a short-lived token query parameter minted by the matching authenticated stream-token operation.
  • The LiveKit webhook carries a LiveKit-signed JWT.
  • Certificate verification, PDF and QR are public and rely on unguessable identifiers.
  • A few onboarding operations are public. They are listed in Public paths.

Each of these is marked on the operation in the reference.

Renew​

curl -sS -X POST "https://<web-host>/api/auth/refresh" --cookie "<cookie jar>"

There is no body. The route reads the startupos_session cookie, redeems it and returns a new {"token", "expiresIn"}. It also rewrites the cookie, because Keycloak may rotate the refresh token. If there is no cookie, or the session has ended, the route returns 401 with {"token": null, "reason": ...} and clears the cookie. If the identity provider cannot be reached it returns 502 and keeps the cookie.

Sign out​

POST /api/auth/logout clears the cookie and ends the Keycloak session. It always returns 200.

Service-to-service​

Services call each other with client-credentials tokens. This is internal and not part of the public API.

Request and response details for the three routes are in the Frontend routes reference.

Sources: frontend/src/app/api/auth/{token,refresh,logout}/route.ts, libs/common-models/src/main/java/com/rtih/startupos/common/security/RoleEnforcementFilter.java (401 and 403 bodies), services/*/src/main/resources/application.yml, infra/oci-devops/deploy/ingress.yaml (catch-all per host).