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).

  1. Your application generates a PKCE code_verifier (cryptographically random, 43-128 chars) and derives code_challenge = BASE64URL(SHA256(code_verifier)).

  2. 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
    
  3. The user authenticates with DotID (PassPort + MFA if required).

  4. DotID redirects back to your registered URI with ?code=<auth-code>&state=<csrf-token>. Verify state matches the one you sent.

  5. 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>
    
  6. 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:

  1. Extract the iss claim from the token header / payload.

  2. 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.

  3. Fetch the realm’s JWKS from <issuer>/protocol/openid-connect/certs. Cache the keys (DotID rotates them on a schedule; respect Cache-Control headers).

  4. Verify the signature against the JWKS, matching on the token’s kid header.

  5. 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).

  6. 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 allowlistSet.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.