API authentication and JWT validation¶
JWT bearer authentication applies to browser/BFF flows and explicit service-audience workload
contracts. It is not the CLI or public tenant-control-plane credential. Protected capability APIs
exported through the public API edge must also accept FGAI-HMAC-SHA256 signed requests from static AKIA
service-user credentials and temporary workforce ASIA STS credentials with a signed
X-FGAI-Security-Token. Human bearer tokens, including platform-admin tokens, are never CLI
credentials; platform administration remains private to the admin-console web app.
This page explains the JWT-bearing surfaces and how a receiving service validates their tokens. See CLI authentication for the signed public-API contract.
Realms¶
DotID issues tokens from named realms on the platform Keycloak. Each realm issues its own JWTs from its own signing key.
Realm |
Who lives there |
Example issuer claim |
|---|---|---|
|
The single tenant entry realm — account-root users, workforce members of accounts, and the platform-level workforce |
Every account and organization is resolved inside flexgalaxy — tenancy rides token claims
(account_id, user_type, org membership), never a realm name. The legacy per-account (acc-*) and
per-org (idc-*) realm model was retired: those realms no longer exist and a token claiming an
acc-*/idc-* issuer cannot be validly issued today. The platform additionally operates
dedicated gate realms for its own surfaces (fgai-admins for platform administration,
staging-access for the staging gate, insider-doc-viewers for the insider portal) — your service
accepts those only if it is one of those platforms. Account-owned service users are non-interactive
and use the Account boundary. These identity populations are isolated and are never merged by
matching email.
Tenant binding: account_id and user_type claims¶
Access tokens issued by the flexgalaxy realm carry two claims that describe the
authenticated subject’s tenant binding: account_id and user_type.
{
"iss": "https://auth.flexgalaxy.ai/auth/realms/flexgalaxy",
"sub": "9e0f6f96-2f57-4f49-84ef-8d4d12d4f5e1",
"account_id": "11111111-1111-1111-1111-111111111111",
"user_type": "root"
}
account_id— the DotID account UUID the subject is bound to. It is emitted for both account-root users and workforce members of an account. It means “this user belongs to this tenant” — nothing more. It is not the account name and not a realm name.user_type— how the subject is bound to that account:root— the account’s root user.workforce— a workforce member of the account.
Rules:
Derive account-root status strictly from
user_type == "root". Never infer root from the mere presence ofaccount_id— a workforce user also carriesaccount_idbut is not a root. Treating “hasaccount_id” as “is root” will silently promote every workforce member to root-equivalent.Use
account_idto resolve the caller’s tenant (for per-request scoping and when building PDP principal context). Do not derive account identity from email, username, or realm name.Unbound
flexgalaxyusers (e.g. platform admins) receive neither claim. Ifaccount_idis absent, the caller is not bound to a tenant; ifuser_typeis absent or is notroot, do not treat the caller as an account root. Route the request through the appropriate platform-admin, workforce, service-user, or workload-identity path instead.
These claims are identity context, not an authorization grant. They tell a service which tenant the validated token belongs to and whether the subject is that account’s root; they do not say the caller may perform a particular service action. A first-party service must still call the DotID PDP for any account/resource operation that is policy controlled.
Validating JWTs your service receives¶
For a route whose manifest explicitly declares JWT bearer authentication, the platform convention
is that the receiving service accepts the allowed realm types declared for that audience. Public
tenant routes on the API edge additionally implement the signed AK/SK and STS path (temporary
ASIA credentials, available to any principal via sts assume-role); JWT support
on another audience does not satisfy that public contract.
Concretely, your JWT validator must:
Extract the
issclaim from the token header / payload.Verify the issuer is the platform realm — exact match:
https://auth.flexgalaxy.ai/auth/realms/flexgalaxy. Reject any other issuer. Do not prefix-match realm families: the legacyacc-*/idc-*families no longer exist, and an exact match is what rejects a rogue realm that merely shares a prefix.Fetch the realm’s JWKS from
<issuer>/protocol/openid-connect/certs. Cache the keys (DotID rotates them on a schedule; respectCache-Controlheaders).Verify the signature against the JWKS, matching on the token’s
kidheader.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 implement exactly this pattern. There is exactly one customer-facing issuer, so
a validator is a single exact-match check on flexgalaxy — there is no open-acceptance or
prefix-allowlist variant to choose between, and none to mix.
Discovering this resource (RFC 9728)¶
Every 401 from the public API edge and from the console API surface names where to find this
resource’s OAuth 2.0 protected-resource metadata:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.flexgalaxy.ai/.well-known/oauth-protected-resource"
Fetch that URL to get the RFC 9728 metadata document for the host you called:
curl -sS https://api.flexgalaxy.ai/.well-known/oauth-protected-resource
{
"resource": "https://api.flexgalaxy.ai",
"bearer_methods_supported": ["header"]
}
Those are the two fields a client needs; the document also carries the other RFC 9728 registry
fields. The advertised URL is always https, always carries no port, and always names the host your
request carried — a client may follow it as-is. The document is public: discovery precedes
authentication, so it needs no credential.
FlexGalaxy.AI is multi-tenant, but tenancy is carried in token claims rather than separate realms:
every account authenticates against the single platform realm, so the metadata document deliberately
does not enumerate authorization servers. The authorization server is the platform realm — use its
/.well-known/openid-configuration.
Common errors¶
Status |
Meaning |
What to check |
|---|---|---|
|
The credential is absent or invalid. |
On JWT-bearing browser/workload routes, verify the bearer token’s |
|
The token was valid but the PDP returned |
Inspect the response body — it includes the |
|
Invalid principal object, action, or resource FRN. |
See FRN format for the canonical resource syntax and parsers. |
|
API versioning mismatch — the request was sent to a deprecated path. |
Public developer APIs use |
Federated workload tokens¶
DotID also issues access tokens through RFC 8693 token exchange for workload identities federated from external OIDC issuers (GitHub Actions, GitLab CI). Validation at your service is identical to validating any other DotID-issued JWT — the same realm JWKS, the same iss / aud / exp rules. The only difference is the token’s sub is a DotID workload identity ID rather than a human or service-user ID, and the token carries an extra federated_subject custom claim of shape {issuer, sub} recording the originating CI subject for audit correlation.
Treat the federated_subject claim as audit metadata only — never use it for authorization decisions. The aud, sub, and scope claims are the authorization surface, just like every other DotID-issued token.
For the exchange contract (the endpoint your CI calls to obtain this token, not the validation your service does to consume it), see workload-federation.md.