Identity
How people sign in through Keycloak and your company's identity provider, how SCIM keeps the directory current, and how every request is checked.
The portal never checks a company password itself. Keycloak is the central identity provider and brokers your company’s directory, Entra ID or Okta. SCIMSCIM provisioningCấp tài khoản qua SCIMThe standard your identity provider uses to create, update and remove portal accounts automatically, so people who leave lose access.Open in glossary keeps the people in the portal in step with that directory.
Key rules
- Keycloak is the only identity provider the portal talks to. It brokers Entra ID or Okta for single sign-onSingle sign-on (SSO)Đăng nhập một lần (SSO)Signing in once with your company account, such as Entra ID or Okta. The portal never sees your password.Open in glossary.
- Applications speak only OIDC. No portal component talks to Entra ID or Okta directly.
- Tokens stay on the server. The backend holds the tokens; the browser holds only a session cookie.
- SCIM provisions people. Your provider pushes joiners, changes and leavers to the portal, which is the SCIM server.
- Every request is checked. The person’s seat is re-read on every request, so a suspension applies on the next request.
The parts
Your deployment holds one organisationOrganisationTổ chứcYour company's space in the portal. Each deployment runs one organisation, inside your own AWS account.Open in glossary, so there is one sign-in path and one company provider behind it.
| Part | What it does |
|---|---|
| Browser | Holds a session cookie and nothing else |
| Portal backend | Holds the tokens, checks every request and serves SCIM |
| Keycloak | Central identity provider; renders the sign-in screens and brokers your provider |
| Entra ID or Okta | Your company directory; authenticates people and pushes SCIM changes |
Which person fields your provider owns and which the portal owns depends on the directory mode. See Departments › Who owns which field and Departments › Directory mode.
The three identity pages
Request authentication
Two global guards run on every route: first the session guard, then the roles guard. Every route is closed unless it is marked public.
The session guard works through these checks in order:
- Session cookie? A browser carries one. A request without a cookie takes the bearer path: the token is verified and its claims are read.
- Does the cookie still resolve? If not, the answer is 401. There is no fallback to the bearer path.
- Is the token near expiry? It is refreshed before the request continues.
- Unsafe method? A request that changes something needs a CSRF header matching the session.
- Seat in the organisation? No seat means 401.
- Suspended? A person disabled at the provider or suspended in the portal gets 403
access_revoked. - Passes. The roles guard then checks the capabilitiesCapabilityQuyền hạnPermission to do one kind of thing, such as setting budgets or reading the audit log. Roles are the only way to get one.Open in glossary the route needs.
| Situation | Outcome |
|---|---|
| No session cookie | Bearer path: the token is verified and its claims parsed |
| Cookie that no longer resolves | 401, with no fallback to the bearer path |
| Token near expiry | Refreshed under a lock |
| Unsafe method | Needs a CSRF header matching the session |
| No seat in the organisation | 401 |
| Provider disabled or portal suspended | 403 access_revoked on the next request; sign-in is refused on the same grounds |
| Token missing a claim | 401, never a 500 |
| Token signing keys unreachable | 503 |
A browser’s context is computed once, at sign-in or refresh. A machine caller is verified on every request. Either way, the seat is read fresh on every request, with no cache.
Why it works this way
Why a suspension answers 403, not 401
The frontend answers a 401 by refreshing the session and signing the person out. A suspended person needs a clear refusal instead, so a suspension returns 403 access_revoked.
Why the seat is re-read on every request
The session carries no department and nothing is cached. Reading the seat on every request means a change of department, role or suspension applies to the very next request, with no wait for a session to expire.
Under the hood
Guard details
- The bearer path verifies the token against the provider’s JWKS; an unreachable JWKS answers 503.
- A token near expiry is refreshed under a lock. A request that loses the race re-reads the holder’s result instead of refreshing again.
- The CSRF header is compared with the session’s value in constant time.
- The seat is one primary-key read that returns both access fields:
status, owned by the provider, andsuspended_at, owned by the portal. Both must allow. - Token parsing returns an identity without a department. The narrower type forces every full request context to read the seat.