API authentication and JWT validation
Every FlexGalaxy.AI API call carries a JWT bearer token issued by DotID. This page explains how the tokens are obtained, which realms issue them, and how your service should validate them.
Authorization Code + PKCE flow
The canonical flow for first-party FlexGalaxy.AI apps and third-party integrations alike is Authorization Code with PKCE (S256).
Your application generates a PKCE code_verifier (cryptographically random, 43-128 chars) and derives code_challenge = BASE64URL(SHA256(code_verifier)).
The user agent is redirected to:
https://auth.flexgalaxy.ai/auth/realms/<realm>/protocol/openid-connect/auth
?response_type=code
&client_id=<your-client-id>
&redirect_uri=<your-registered-redirect-uri>
&scope=openid+profile+email
&state=<csrf-token>
&code_challenge=<challenge>
&code_challenge_method=S256
The user authenticates with DotID (PassPort + MFA if required).
DotID redirects back to your registered URI with ?code=<auth-code>&state=<csrf-token>. Verify state matches the one you sent.
Your application exchanges the code for tokens:
POST https://auth.flexgalaxy.ai/auth/realms/<realm>/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<auth-code>
&redirect_uri=<same-uri-as-step-2>
&client_id=<your-client-id>
&client_secret=<secret> # confidential clients only
&code_verifier=<original-verifier>
DotID returns access_token, id_token, and (if offline_access was requested) refresh_token. All three are JWTs.
The access_token is what you present to FlexGalaxy.AI APIs as Authorization: Bearer <token>. The id_token is for your own application to learn who the user is. The refresh_token is exchanged at the same /token endpoint with grant_type=refresh_token.
Realms
DotID uses four realm types. Each one issues its own JWTs from its own signing key.
Realm |
Who lives there |
Example issuer claim |
master
|
Break-glass Keycloak admin only; not used for normal platform-admin login |
https://auth.flexgalaxy.ai/auth/realms/master
|
flexgalaxy
|
Platform admins (FlexGalaxy operators), account-root users, and platform-level workforce |
https://auth.flexgalaxy.ai/auth/realms/flexgalaxy
|
acc-<account-id>
|
Per-account service users |
https://auth.flexgalaxy.ai/auth/realms/acc-029cea77800e
|
idc-<account-id>-<region>
|
Per-account Identity Center personas (SSO) |
https://auth.flexgalaxy.ai/auth/realms/idc-029cea77800e-ap1
|
Tenants get a fresh acc-* realm and a fresh idc-* realm at account provisioning time. The realms are isolated — a user in one acc-* realm cannot sign in to another.
Validating JWTs your service receives
FlexGalaxy.AI’s platform convention is that every service accepts tokens from any of the four realm types. A service that rejects a valid token because it came from an idc-* realm rather than flexgalaxy breaks the cross-realm composition the platform depends on.
Concretely, your JWT validator must:
Extract the iss claim from the token header / payload.
Verify the issuer URL is one of: https://auth.flexgalaxy.ai/auth/realms/{master,flexgalaxy} or https://auth.flexgalaxy.ai/auth/realms/acc-* or https://auth.flexgalaxy.ai/auth/realms/idc-*. Reject any other issuer.
Fetch the realm’s JWKS from <issuer>/protocol/openid-connect/certs. Cache the keys (DotID rotates them on a schedule; respect Cache-Control headers).
Verify the signature against the JWKS, matching on the token’s kid header.
Verify the standard claims: exp (not expired), nbf (if present, not before), iat (sanity-check against clock skew), aud (matches your service’s audience identifier).
Verify any service-specific claims your authorization logic depends on (e.g. realm_access.roles, groups).
DotID’s own services (authorization, audit) use a MultiRealmJwtDecoder that implements exactly this pattern. Two acceptable approaches exist:
Open acceptance — accept any realm whose JWKS endpoint resolves and serves valid keys. Used by DotID Authorization, DotID Audit, and TrustMint.
Explicit allowlist — Set.of("flexgalaxy", "master") plus a prefix match for acc-* / idc-*. Used by Bazaar and OTA. Slightly more secure (rejects rogue realms even if they reach the auth server) at the cost of a config update when a new realm category is added.
Pick one approach per service and document it. Mixing the two within one service leads to subtle authorization gaps.
AuthorizeRequest / AuthorizeResponse shape
Calling the DotID Policy Decision Point (PDP) from your service uses a fixed request/response contract — see ADR-0018 for the full specification.
Minimal request:
{
"principal": {
"account_id": "acc-029cea77800e",
"user_id": "u-12345",
"user_type": "iam",
"ic_session": false,
"ps_id": null
},
"action": "licensing:Device:Read",
"resource": "frn:acc-029cea77800e:licensing:device/dev-12345",
"context": {
"source_ip": "203.0.113.42",
"request_time": "2026-05-25T11:00:00Z"
}
}
Minimal response:
{
"decision": "ALLOW",
"reason": null,
"matched_statement": "AllowDeviceRead",
"evaluation_time_ms": 2
}
The decision is always ALLOW or DENY. The reason is null on allow and a stable machine-readable code on deny; matched_statement is the matching policy statement Sid, or null when no statement matched. DotID serializes Java record fields with snake_case JSON names.
Common errors
Status |
Meaning |
What to check |
401 Unauthorized
|
No bearer token, or the token signature did not validate. |
Verify Authorization: Bearer <token> is present and the token’s iss/kid resolved to a valid JWKS key. |
403 Forbidden
|
The token was valid but the PDP returned DENY. |
Inspect the response body — it includes the reason and (if available) matched_statement to trace the deny. |
400 Bad Request (PDP)
|
Invalid principal object, action, or resource FRN. |
See FRN format for the canonical resource syntax and parsers. |
400 Bad Request (DotID API)
|
API versioning mismatch — the request was sent to a deprecated path. |
All current APIs live under /v1/; older paths return 400. |