OAuth Token Exchange audience

78 views
Skip to first unread message

Sebastian Sdorra

unread,
Mar 3, 2026, 10:03:06 AM (3 days ago) Mar 3
to CAS Community
Hello,

I'm unsure whether I'm misunderstanding something, making an error, or encountering a bug.
I'm trying to use OAuth Token Exchange with CAS, but CAS returns an incorrect audience value.

I'm using CAS in version 7.3.4 and i have two services: oidc-001 (localhost:5001) and oidc-002 (localhost:5002).
oidc-001 needs to perform a token exchange to obtain an AccessToken for oidc-002.

Here's my current process:

I perform an authorization code flow for oidc-001 and receive a JWT AccessToken from CAS.
I use this AccessToken as a bearer token to access the oidc-001 API.
The API extracts the JTI and initiates an OAuth token exchange with the JTI,
the audience of oidc-002, and a resource URL that matches the serviceId of oidc-002.

The exchange completes successfully,
but it returns an AccessToken with an audience of oidc-001.
I would expect the audience to be oidc-002 instead.

The request to oidc-001:

GET /proxy HTTP/1.1
Host: localhost:5001
User-Agent: curl/8.7.1
Accept: */*
Authorization: Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImNhcy1haFdMYUxqTSIsIm9yZy5hcGVyZW8uY2FzLnNlcnZpY2VzLlJlZ2lzdGVyZWRTZXJ2aWNlIjoiNTAwMSJ9.eyJhdWQiOiJvaWRjLTAwMSIsInN1YiI6InRyaWxsaWFuIiwiZ3JhbnRfdHlwZSI6ImF1dGhvcml6YXRpb25fY29kZSIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA5MC9jYXMvb2lkYyIsInJlc3BvbnNlX3R5cGUiOiJub25lIiwiZXhwIjoxNzcyNTYxNzgzLCJpYXQiOjE3NzI1MzI5ODMsImp0aSI6IkFULTE0LUpmbi1QRUI0Ujdzano5TnJKR1BJZnAxZHNRdnZqb21yLW9yYnN0YWNrIn0.MG6D5aqbSyGb-FOaSVfQOgVJI54tpc9j_dHdkspMhsyg9MRyZDWA6c0lh9zI-H-Ze12u9cOqMexYFF4CU6EQykx5TISz61UU0C2GZJizrA7c1z-_GrdozbAs1a02mVHCjZoeS-GAr2DIY8mAD_Guykt623ykIW7ML1Dya6rKLKCJayigYOut8vXRMTne50DwOB0gHHnKrHuoGf7YCO-1QUZbYQOhe021nvx2cXiSyOF1rYMOdhNRco23ceQonC3DhzIvn_BpnRqSQunjE_pV0BzEJAQzBa3KZE6Uylii1y7SAOV3R_jkUtKycbC2_h7N6Z_LMu2lW3eH1VaoYWOK2Q

The decoded AccessToken looks like this:

{
  "aud": "oidc-001",
  "sub": "trillian",
  "grant_type": "authorization_code",
  "iss": "http://localhost:8090/cas/oidc",
  "response_type": "none",
  "exp": 1772561783,
  "iat": 1772532983,
  "jti": "AT-14-Jfn-PEB4R7sjz9NrJGPIfp1dsQvvjomr-orbstack"
}

The service oidc-001 makes now the request for the token exchange:

POST /cas/oidc/oidcAccessToken HTTP/1.1
Host: localhost:8090
User-Agent: Go-http-client/1.1
Content-Length: 319
Content-Type: application/x-www-form-urlencoded

audience=service-d&client_id=oidc-001&client_secret=secret&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=http%3A%2F%2Flocalhost%3A5002%2F&scope=openid&subject_token=AT-14-Jfn-PEB4R7sjz9NrJGPIfp1dsQvvjomr-orbstack&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token

The fields better formatted:
audience: service-d
client_id: oidc-001
client_secret:secret
grant_type: urn:ietf:params:oauth:grant-type:token-exchange
resource: http://localhost:5002/
scope: openid
subject_token: AT-14-Jfn-PEB4R7sjz9NrJGPIfp1dsQvvjomr-orbstack
subject_token_type: urn:ietf:params:oauth:token-type:access_token

Now we get the response from CAS:

HTTP/1.1 200
Server: Apereo CAS.

{"access_token":"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImNhcy1haFdMYUxqTSIsIm9yZy5hcGVyZW8uY2FzLnNlcnZpY2VzLlJlZ2lzdGVyZWRTZXJ2aWNlIjoiNTAwMSJ9.eyJhdWQiOiJvaWRjLTAwMSIsInN1YiI6InRyaWxsaWFuIiwiZ3JhbnRfdHlwZSI6ImF1dGhvcml6YXRpb25fY29kZSIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA5MC9jYXMvb2lkYyIsInJlc3BvbnNlX3R5cGUiOiJub25lIiwiZXhwIjoxNzcyNTYyMzI1LCJpYXQiOjE3NzI1MzM1MjUsImp0aSI6IkFULTIyLWhiVG0yTkg2MTdKQ3MzUEdSSXNYYTNDZTNFMkphcUlvLW9yYnN0YWNrIn0.h4OvaThjPalkDTfE3WehbXJ4fzjpKSIX_I949j-pRIt71l9Knm_uYgOzcLZSR-n7pWzmquNlqNY9UOQBcO9TqNV2dC9od-eJTTMBUo2q1WVMiRqWBWLpK8-mR3SIZBjxiE5tY-es2LMyd3h_CH6646xqM98DIersYG6oYygg4FFslnagVDcdJlR8vouxDU0pEkBXyTOSPTXubdVvyYRM79fMiqgYPZmsMfRL9MqrB91vcljAN84bXJxwhCdtbnlLoTjtvEwW_6xKJv9XaBM97AJa9BUt5KdUlDqHCwVhQPzal4WHGm2a8osu1JPrm5zLQ_0xBxhzgB_hFEaMya37tg","token_type":"Bearer","expires_in":28800,"issued_token_type":"urn:ietf:params:oauth:token-type:access_token","scope":"openid"}.

The decoded AccessToken:

{
  "aud": "oidc-001",
  "sub": "trillian",
  "grant_type": "authorization_code",
  "iss": "http://localhost:8090/cas/oidc",
  "response_type": "none",
  "exp": 1772562325,
  "iat": 1772533525,
  "jti": "AT-22-hbTm2NH617JCs3PGRIsXa3Ce3E2JaqIo-orbstack"
}

As we can see the access token has oidc-001 as audience instead of oidc-002.

The configuration of service oidc-001:

{
    "@class": "org.apereo.cas.services.OidcRegisteredService",
    "clientId": "oidc-001",
    "clientSecret": "secret",
    "serviceId": "^http://localhost:5001/.*",
    "name": "oidc-001",
    "id": 5001,
    "jwtAccessToken": true,
    "audience": [
        "java.util.HashSet",
        [
            "oidc-001"
        ]
    ],
    "bypassApprovalPrompt": true,
    "supportedResponseTypes": [
        "java.util.HashSet",
        [
            "code"
        ]
    ],
    "supportedGrantTypes": [
        "java.util.HashSet",
        [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:token-exchange"
        ]
    ],
    "scopes": [
        "java.util.HashSet",
        [
            "profile",
            "openid",
            "email"
        ]
    ],
    "tokenExchangePolicy": {
        "@class": "org.apereo.cas.support.oauth.services.DefaultRegisteredServiceOAuthTokenExchangePolicy",
        "allowedActorTokenTypes": [
            "java.util.HashSet",
            [
                "urn:ietf:params:oauth:token-type:access_token",
                "urn:ietf:params:oauth:token-type:jwt"
            ]
        ],
        "allowedResources": [
            "java.util.HashSet",
            [
                "http://localhost:5002/.*"
            ]
        ]
    },
    "usernameAttributeProvider": {
        "@class": "org.apereo.cas.services.DefaultRegisteredServiceUsernameProvider"
    },
    "attributeReleasePolicy": {
        "@class": "org.apereo.cas.services.ReturnAllAttributeReleasePolicy"
    }
}

The configuration of oidc-002:

{
    "@class": "org.apereo.cas.services.OidcRegisteredService",
    "clientId": "oidc-002",
    "clientSecret": "secret",
    "serviceId": "^http://localhost:5002/.*",
    "name": "oidc-002",
    "id": 5002,
    "jwtAccessToken": true,
    "audience": [
        "java.util.HashSet",
        [
            "oidc-002"
        ]
    ],
    "bypassApprovalPrompt": true,
    "supportedResponseTypes": [
        "java.util.HashSet",
        [
            "code"
        ]
    ],
    "supportedGrantTypes": [
        "java.util.HashSet",
        [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:token-exchange"
        ]
    ],
    "scopes": [
        "java.util.HashSet",
        [
            "profile",
            "openid",
            "email"
        ]
    ],
    "tokenExchangePolicy": {
        "@class": "org.apereo.cas.support.oauth.services.DefaultRegisteredServiceOAuthTokenExchangePolicy",
        "allowedActorTokenTypes": [
            "java.util.HashSet",
            [
                "urn:ietf:params:oauth:token-type:access_token",
                "urn:ietf:params:oauth:token-type:jwt"
            ]
        ],
        "allowedResources": [
            "java.util.HashSet",
            [
                "http://localhost:5001/.*"
            ]
        ]
    },
    "usernameAttributeProvider": {
        "@class": "org.apereo.cas.services.DefaultRegisteredServiceUsernameProvider"
    },
    "attributeReleasePolicy": {
        "@class": "org.apereo.cas.services.ReturnAllAttributeReleasePolicy"
    }
}

Is this the expected behavior? Is my request or configuration incorrect? Or is this a bug?
Reply all
Reply to author
Forward
0 new messages