Identity Assertion Authorization Grant (Cross-App Access)#
Overview#
The Janssen Authorization Server implements the Identity Assertion Authorization Grant (draft-ietf-oauth-identity-assertion-authz-grant-04), also known as Cross-App Access (XAA).
This mechanism enables a client already authenticated with one Identity Provider (IdP) to obtain access tokens from a different Resource Authorization Server (Resource AS) that trusts the same IdP, without launching a new browser-based SSO flow. It combines:
- OAuth 2.0 Token Exchange (RFC 8693) to issue an Identity Assertion JWT (ID-JAG), and
- JWT Bearer Token Grant (RFC 7523) to exchange the ID-JAG for an access token.
Protocol Flow#
Client IdP Janssen-AS Resource Janssen-AS
| | |
|-- 1. OIDC login ----------->| |
|<-- ID token / RT -----------| |
| | |
|-- 2. Token Exchange ------->| |
| grant_type=token-exchange | |
| requested_token_type= | |
| oauth:token-type:id-jag | |
| subject_token=<ID token> | |
| audience=<Resource AS> | |
|<-- 3. ID-JAG JWT -----------| |
| typ: "oauth-id-jag+jwt" | |
| | |
|-- 4. JWT Bearer grant ------------------------------> |
| grant_type=jwt-bearer |
| assertion=<ID-JAG> |
|<-- access_token -----------------------------------------|
Janssen AS can play two roles simultaneously:
| Role | What Janssen AS does |
|---|---|
| IdP AS | Receives token exchange requests and issues ID-JAG JWTs |
| Resource AS | Validates ID-JAG bearer assertions and issues access tokens |
Enabling the Feature#
The entire feature is controlled by the identity_assertion_authz_grant feature flag.
Add it to featureFlags in the Janssen authorization server configuration:
{
"featureFlags": ["identity_assertion_authz_grant"]
}
When the flag is present, both roles are activated and the discovery document is updated automatically.
Configuration Fields#
| Field | Type | Default | Description |
|---|---|---|---|
idJagTrustedIdpIssuers |
Map<String, TrustedIssuerConfig> |
{} |
(Resource AS role) Trusted IdP issuers whose ID-JAGs this AS will accept. Empty map means any issuer is accepted. |
idJagLifetime |
int (seconds) |
300 |
(IdP role) Lifetime for issued ID-JAGs. |
idJagIssueRefreshToken |
Boolean |
false |
(Resource AS role) Whether to issue refresh tokens after accepting an ID-JAG. The spec recommends false. |
Restricting trusted IdP issuers#
To restrict which IdPs can issue ID-JAGs accepted by this AS (Resource AS role):
{
"idJagTrustedIdpIssuers": {
"https://idp.example.com": {
"automaticallyGrantedScopes": ["openid", "profile"]
}
}
}
Verifying ID-JAGs issued by another server#
By default the ID-JAG signature is verified with this AS's own keys (same-instance deployments). For an ID-JAG issued by a different server (for example another Janssen/Gluu instance or Keycloak), configure the issuer's signing keys in its idJagTrustedIdpIssuers entry:
| Field | Description |
|---|---|
jwks |
Inline JWKS (JSON string). Use for issuers not reachable from this AS. Takes precedence over jwksUri. |
jwksUri |
https URL of the issuer's JWKS. Used when jwks is blank. Must be allowed by externalUriWhiteList if that is configured. |
{
"idJagTrustedIdpIssuers": {
"https://idp.example.com": {
"jwksUri": "https://idp.example.com/jans-auth/restv1/jwks"
},
"https://keycloak.example.org/realms/lab": {
"jwks": "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"key-1\",\"alg\":\"RS256\",\"use\":\"sig\",\"n\":\"...\",\"e\":\"AQAB\"}]}"
}
}
}
Behavior:
- The signature key is selected by the JWT
kid, from the keys of the issuer named iniss. The issuer must be inidJagTrustedIdpIssuersbefore any key is loaded. - Keys fetched from
jwksUriare cached for 300 seconds. An unknownkidtriggers a refetch, at most once per 30 seconds. A failed fetch is not retried for 30 seconds. - If
jwksorjwksUriis configured but keys cannot be obtained, the ID-JAG is rejected. There is no fallback to this AS's own keys. - If neither field is set, this AS's own keys are used.
- The same
TrustedIssuerConfigtype is used bytrustedSsaIssuers;jwksandjwksUriare ignored there.
Step 2: Token Exchange Request (IdP Role)#
The client sends a token exchange request to the IdP's /token endpoint:
POST /jans-auth/restv1/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client_credentials>
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&subject_token=<ID_TOKEN>
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&audience=https://resource-as.example.com
&scope=openid profile
Supported subject_token_type values:
| Value | Description |
|---|---|
urn:ietf:params:oauth:token-type:id_token |
OIDC ID token (most common) |
urn:ietf:params:oauth:token-type:saml2 |
SAML 2.0 assertion |
urn:ietf:params:oauth:token-type:refresh_token |
Refresh token (for SAML-first deployments) |
Response:
{
"access_token": "<signed_ID-JAG_JWT>",
"issued_token_type": "urn:ietf:params:oauth:token-type:id-jag",
"token_type": "N_A"
}
ID-JAG JWT Structure#
The issued ID-JAG is a signed JWT with:
Header:
{
"typ": "oauth-id-jag+jwt",
"alg": "RS256",
"kid": "<key_id>"
}
Claims:
{
"iss": "https://idp.example.com",
"sub": "alice",
"aud": "https://resource-as.example.com",
"client_id": "my-app",
"jti": "550e8400-e29b-41d4-a716-446655440000",
"exp": 1716648300,
"iat": 1716648000,
"scope": "openid profile",
"email": "alice@example.com"
}
Optional claims propagated from the subject token: email, auth_time, acr, amr.
Step 4: JWT Bearer Request (Resource AS Role)#
The client presents the ID-JAG to the Resource AS /token endpoint:
POST /jans-auth/restv1/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client_credentials>
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<ID-JAG_JWT>
&scope=read write
Validation performed by Resource AS:
- JWT
typheader ="oauth-id-jag+jwt" issis inidJagTrustedIdpIssuers(if configured)- Signature verification, using the trusted issuer's
jwksorjwksUrikeys when configured, otherwise this AS's own keys audmatches this AS's issuerclient_idmatches the authenticated clientexphas not passed
Subject resolution order: sub → email → aud_sub → empty user
Response:
{
"access_token": "<access_token>",
"token_type": "Bearer",
"expires_in": 300,
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}
Note: no refresh_token is issued by default (controlled by idJagIssueRefreshToken).
Discovery Metadata#
When the feature flag is enabled, the following fields appear in the
OpenID Configuration (/.well-known/openid-configuration):
{
"identity_chaining_requested_token_types_supported": [
"urn:ietf:params:oauth:token-type:id-jag"
],
"authorization_grant_profiles_supported": [
"urn:ietf:params:oauth:grant-profile:id-jag"
]
}
Security Considerations#
- Confidential clients only. Public clients should use the authorization code flow. The IdP AS should reject token exchange requests from non-confidential clients.
- No refresh tokens. The Resource AS should not issue refresh tokens for ID-JAG exchanges. When the access token expires, the client should re-present the ID-JAG or obtain a new one.
- Audience binding. The ID-JAG
audmust exactly match the Resource AS issuer URI. This prevents replay across different Resource Servers. - Client ID binding. The
client_idin the ID-JAG must match the authenticated client at the Resource AS. This prevents impersonation. - Trusted issuer list. Configure
idJagTrustedIdpIssuersto restrict which IdPs can issue ID-JAGs your Resource AS accepts. - Short lifetime. Keep
idJagLifetimeshort (default 5 minutes). ID-JAGs are single-use delegation tokens.