仓库指标
- 星标
- (34,398 个星标)
- PR 合并指标
- (平均合并 6天 19小时) (30 天内合并 384 个 PR)
描述
Describe the bug
Breaking change in 'aud' audience content for access tokens
Previously on Keycloak 16 I used to ensure that audience resolution in access tokens contained the clientId of the current clientId. I was also checking that the various OIDC client libraries I used had this audience check (and that was far than always the case).
This audience check is for example present in keycloak-connect (nodejs):
if (expectedType === 'ID') {
if (!audienceData.includes(this.clientId)) {
reject(new Error('invalid token (wrong audience)'));
}
if (token.content.azp && token.content.azp !== this.clientId) {
reject(new Error('invalid token (authorized party should match client id)'));
}
} else if (this.verifyTokenAudience) {
if (!audienceData.includes(this.clientId)) {
reject(new Error('invalid token (wrong audience)'));
}
}
As you can see in this code the audience check is not done for the Id Token only, it also include a check in the access token audience.
I am now testing keycloak 18, and a change has been made on access token audience mapper resolver, dating this https://issues.redhat.com/browse/KEYCLOAK-8483 I think.
The access token "aud" is now only listing 'reachable' services, like clientIds of clients where the current user has at least one role associated with the client. But the RP client id is never on the "aud" list, even if the current user has a role linked to the current client.
As stated in the linked issue one solution would be to add a 'static audience' mapper, adding the current client Id on the response (which would be added to the list of reachable services for this access token). The problem is that I do not know any way of having this client Id restricted to only users having a role on this client, it's not dynamic.
Version
18
Expected behavior
Let's take an example.
We have 2 confidential client
- conf-client-1
- conf-client-2
Each client has roles, to keep it simple let's just add one role only:
- conf-client-1 : role "access conf-client-1"
- conf-client-2 : role "access conf-client-2"
The confidential application are not in full scope mode, a forced client scope 'test' is added, containing a mapping to these two roles.
The test user "user1" has only the "access conf-client-1" role. The test user "user2" has the "access conf-client-1" and "access conf-client-2" roles.
Previously, with the audience resolver mapper present in the role client scope the result of access tokens generated for connections on the confidential applications would have been:
- user1 on conf-client-1 : Access token: "aud": "conf-client-1"
- user1 on conf-client-2 : Access token: "aud": "conf-client-1" <-- current RP is absent, user has no role on conf-client-2
- user2 on conf-client-1 : Access token: "aud": ["conf-client-1", "conf-client2"]
- user2 on conf-client-2 : Access token: "aud": ["conf-client-1", "conf-client2"]
Actual behavior
Now the result is:
- user1 on conf-client-1 : Access token: no "aud" key <-- current RP is absent
- user1 on conf-client-2 : Access token: "aud": "conf-client-1" <-- current RP is absent
- user2 on conf-client-1 : Access token: "aud": "conf-client-2" <-- current RP is absent
- user2 on conf-client-2 : Access token: "aud": "conf-client-1" <-- current RP is absent
If I add a static echo of the current RP with a static audience mapper I will obtain:
- user1 on conf-client-1 : Access token: "aud": "conf-client-1"
- user1 on conf-client-2 : Access token: "aud": ["conf-client-1", "conf-client2"] <- now current RP is always there
- user2 on conf-client-1 : Access token: "aud": ["conf-client-1", "conf-client2"]
- user2 on conf-client-2 : Access token: "aud": ["conf-client-1", "conf-client2"]
And now it seems everyone has the audience OK (but user1 on application 2 should not have had this audience accepted).
How to Reproduce?
No response
Anything else?
I know a way of preventing access for users with no associated roles on the application (altering the browser flow to add cheks on roles on cookie access and form access -- without this the default browser flow will never prevent an Id token containing the RP in aud, even if the user has no role on the client) but that's not a simple task. It seems to me that adding an audience check on access token was a good way of adding of first layer of security, if the current RP is not on the access token then maybe the user has no role on this client and the access token is invalid (of course you can also check the roles on the on the token, but that's a different task). And having all application doing the 'simple' audience check was an easy security improvment.
As said in the OIDC standard:
https://openid.net/specs/openid-connect-core-1_0.html
16.8. Access Token Redirect
An Attacker uses the Access Token generated for one resource to obtain access to a second resource.
To mitigate this threat, the Access Token SHOULD be audience and scope restricted. One way of implementing it is to include the identifier of the resource for whom it was generated as audience. The resource verifies that incoming tokens include its identifier as the audience of the token.
So, really, I do not understand the KEYCLOAK-8483 rule: "The aud claim should not include the RP by default. The access token aud should only contain services."
Bearer-only services have to ensure the access token contain the clientId. And if the access token was generated for a public client, or a different confidential client, we should have this bearer-Only service in the aud, OK. But if the rule is different for access tokens directly received by a confidential or public service (do not check the aud, as you are never inside), I think the audience check will become so complex that every developper solution will be to ignore audience check, and that's not good IMHO.