> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt
# ID-JAG (XAA) authentication
ID-JAG (Identity Assertion JWT Authorization Grant, also called [XAA](https://xaa.dev)) lets your organization's identity provider (IdP) — like Okta, Microsoft Entra ID, or any OIDC-compliant SSO — issue short-lived tokens that grant programmatic access to PostHog. Instead of each user creating a [personal API key](/docs/api.md#authentication) or running an [OAuth](/docs/api/oauth.md) flow, your IdP issues an **ID-JAG** that's exchanged with PostHog for a short-lived access token. Access is granted, scoped, and revoked centrally in your IdP.
**ID-JAG/XAA is in beta**
ID-JAG/XAA is currently in `beta`. This means the flow and configuration are subject to change, and the underlying ID-JAG spec is still evolving.
**Enterprise plan only**
ID-JAG (XAA) is only available on the Enterprise plan, and isn't enabled for every Enterprise customer yet. If you're interested, reach out to your technical account manager (TAM) or the[](/teams/platform-features.md)
[](/teams/platform-features.md)
[
](/teams/platform-features.md)
[Platform Features Team
Platform Features Team](/teams/platform-features.md) team to get access.
## Why use it
ID-JAG keeps authorization decisions in your IdP instead of spreading them across personal keys and per-user OAuth grants:
- **No personal API keys to manage** — users authenticate with their existing corporate SSO, and tokens are minted on demand.
- **Centralized policy** — your IdP evaluates its own access policies (group membership, conditional access, etc.) before issuing an ID-JAG.
- **Centralized revocation** — once your IdP stops issuing ID-JAGs for a user, they can no longer obtain new access tokens. Existing tokens expire on their own within a short lifetime.
- **Scoped tokens** — the issued access token carries only the scopes your IdP granted, intersected with what the client requested.
The most common use is connecting AI agents to the [PostHog MCP server](/docs/model-context-protocol.md) under central control. See [enterprise-managed authorization](/docs/model-context-protocol/enterprise-managed-authorization.md) for the MCP-specific setup.
## How it works
sequenceDiagram participant User as Browser participant Client as Client app participant IdP as Your IdP participant AS as PostHog Auth Server
(oauth.posthog.com) participant API as PostHog API Client->>User: Redirect to IdP for SSO login User->>IdP: Log in with corporate credentials IdP-->>Client: ID token (Identity Assertion) Client->>IdP: Exchange ID token for an ID-JAG Note over IdP: Evaluate access policy IdP-->>Client: ID-JAG (JWT) Client->>AS: Token request with ID-JAG
(JWT Bearer grant) Note over AS: Validate ID-JAG signature,
audience, resource, scopes AS-->>Client: PostHog access token loop API calls Client->>API: Call API with access token API-->>Client: Result end
PostHog validates each ID-JAG against the trusted IdP's public keys (via OIDC discovery or a JWKS URL), checks its audience, resource, issuer, and expiration, confirms the user is an active member of the organization that owns the verified domain, then mints a short-lived access token.
## Requirements
- An **Enterprise plan with the XAA authentication feature** enabled on your PostHog organization. [Contact us](/talk-to-a-human.md) if you're not sure whether your plan includes it.
- A **verified domain** in PostHog for the email domain your users sign in with.
- An **IdP that can issue ID-JAG tokens** and publishes an OIDC discovery document (or JWKS endpoint) that PostHog can reach.
## Setup
### 1\. Verify your domain in PostHog
Go to [**Organization settings → Authentication domains & SSO**](https://app.posthog.com/settings/organization-authentication) and add and verify the domain your users authenticate with (e.g. `example.com`). See the [SSO docs](/docs/settings/authentication/sso.md) for domain verification steps.
ID-JAG only accepts tokens for users who are **active members of the organization that owns the verified domain**, so the user's email domain must match.
### 2\. Configure the trusted IdP
On the verified domain, open the **Configure XAA (ID-JAG)** modal and fill in:
| Field | Required | Description |
| --- | --- | --- |
| **IdP issuer URL** | Yes | Your IdP's issuer URL. Must exactly match the `iss` claim on the ID-JAG tokens it issues (e.g. `https://idp.example.com`). Setting this enables ID-JAG for the domain. |
| **JWKS URL** | No | Override the key set location. Defaults to OIDC discovery at `{issuer}/.well-known/openid-configuration`. |
| **Allowed client IDs** | No | Restrict which OAuth `client_id` values are accepted. Leave empty to allow any client. |
PostHog uses the issuer URL to fetch your IdP's public keys and verify every ID-JAG signature. Internal, loopback, and metadata-host URLs are rejected.
### 3\. Grant the required scopes in your IdP
Configure your IdP to include the scopes each integration needs when it issues ID-JAGs. At minimum, all tokens need `user:read`. For the [MCP server](/docs/model-context-protocol/enterprise-managed-authorization.md), also include `organization:read` and `project:read`. Add any additional [API scopes](/docs/api.md#scopes) for what you want to do (for example, `feature_flag:write` to manage flags).
PostHog issues the access token with the **intersection** of the scopes your IdP granted and the scopes the client requested.
## Exchanging an ID-JAG for an access token
Clients exchange an ID-JAG at PostHog's OAuth token endpoint using the [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523) JWT Bearer grant:
- **Cloud:** `https://oauth.posthog.com/oauth/token/`
- **Self-hosted:** `{your-instance-url}/oauth/token/`
Terminal
```bash
curl -X POST https://oauth.posthog.com/oauth/token/ \
-d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
-d assertion= \
-d scope="user:read organization:read project:read"
```
A successful response returns a short-lived access token:
JSON
```json
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "user:read organization:read project:read"
}
```
Use it as a bearer token on PostHog API requests: `Authorization: Bearer `.
Most enterprise MCP clients perform this exchange automatically — you usually don't call the token endpoint by hand. See [enterprise-managed authorization](/docs/model-context-protocol/enterprise-managed-authorization.md) for the MCP flow.
## Tokens
The access token PostHog mints from a valid ID-JAG is:
- **Short-lived** — 2 hours by default. The client repeats the ID-JAG exchange to get a fresh token; there's no refresh token.
- **Audience-restricted** — bound to the resource the ID-JAG targeted, so it can't be replayed against other audiences.
- **Organization-scoped** — bound to the organization that owns the verified domain and the IdP config.
- **Single-use per assertion** — each ID-JAG's `jti` is recorded, so a captured ID-JAG can't be replayed.
## Revoking access
Because authorization lives in your IdP, **revoking a user's access there takes effect immediately** — once your IdP stops issuing ID-JAGs for a user, the client can no longer obtain new access tokens, and existing tokens expire within the short token lifetime. There's nothing to revoke per-client or per-device in PostHog.
## Troubleshooting
The token endpoint returns standard OAuth errors ([RFC 6749 §5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2)). Common ones:
| Error | What it means |
| --- | --- |
| `access_denied` | The organization doesn't have the XAA authentication feature enabled. |
| `invalid_grant` (`ID-JAG could not be verified`) | No verified domain matches the token's email/issuer, the issuer doesn't match the configured IdP, the email isn't verified, or the user isn't an active member of the organization. The message is intentionally generic — check PostHog logs and your IdP config. |
| `invalid_grant` (`ID-JAG has expired` / `not yet valid`) | Clock skew between your IdP and PostHog (tolerance is 30 seconds by default), or an expired assertion. |
| `invalid_target` | The token's `resource` claim doesn't match an allowed resource. Confirm the client derived the resource from PostHog's discovery metadata. |
| `invalid_client` | The token's `client_id` isn't in the domain's allowed client IDs list. |
| `insufficient_scope` (from the resource server) | The granted scopes don't cover what the request needs. Add the missing scopes in your IdP. |
## Further reading
- [Enterprise-managed authorization for MCP](/docs/model-context-protocol/enterprise-managed-authorization.md)
- [ID-JAG / XAA specification](https://xaa.dev)
- [SSO](/docs/settings/authentication/sso.md)
- [OAuth integration](/docs/api/oauth.md)
- [API authentication and scopes](/docs/api.md#authentication)
### Still have questions?
Ask PostHog AI
### Was this page useful?
HelpfulCould be better