Skip to content

Initial experimental support for Resource Indicators#46763

Merged
stianst merged 2 commits intokeycloak:mainfrom
stianst:resource-indicators-poc
Mar 17, 2026
Merged

Initial experimental support for Resource Indicators#46763
stianst merged 2 commits intokeycloak:mainfrom
stianst:resource-indicators-poc

Conversation

@stianst
Copy link
Copy Markdown
Contributor

@stianst stianst commented Mar 3, 2026

Initial support for resource indicators by filtering token audience as proposed in:
#35743 (comment)

This is just the start and I've created a list of follow-ups in #47040 (comment) will convert these to sub-tasks of #14355 once this PR is merged. For those reviewing it would be good to capture anything else that is missing, a comment in this PR would be fine (I'll create tasks afterwards).

Signed-off-by: stianst [email protected]

@stianst stianst force-pushed the resource-indicators-poc branch 2 times, most recently from 5a2655b to 8fc755b Compare March 3, 2026 10:01
@stianst stianst requested review from mposolda and rmartinc March 3, 2026 11:07
Copy link
Copy Markdown
Contributor

@mposolda mposolda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stianst Added some review comments. I assume most of them can be covered as a follow-up, but just checking...


@Override
public AccessToken intercept(AccessToken accessToken, ClientSessionContext clientSessionCtx) {
if (!Profile.isFeatureEnabled(Profile.Feature.RESOURCE_INDICATORS)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check seems a bit redundant? As ResourceIndicatorsInterceptorFactory is enabled just if the feature RESOURCE_INDICATORS feature is enabled, so it is already checked at the "factory" level.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will remove from the updates

try {
responseBuilder = tokenManager
.responseBuilder(realm, client, event, session, userSession, clientSessionCtx).generateAccessToken();
} catch (TokenInterceptorException e) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are few more places from where tokenManager.responseBuilder(..).generateAccessToken() is invoked. I suppose you want to address that as a follow-up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was mostly just temporary. I've moved it to be handled in TokenEndpoint#processGrantRequest so it doesn't have to be handled in each grant separately.

for (String audience : accessToken.getAudience()) {
ClientModel client = session.clients().getClientByClientId(session.getContext().getRealm(), audience);
if (client != null) {
String clientResourceUrl = client.getAttribute("resource_url");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Do we want to support explicit client attribute like resource_url ? Or do we eventually want to support also something like "Base URL" of the client as fallback in case that resource_url attribute is not set?

I recall in the original proposal google docs, there was suggestion to use "Base URL" (or "Root URL") of a client as a fallback. But maybe this is not needed and for the sake of simplicity and safety, we can rather always require resource_url client attribute?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I'm just using a custom attribute, but will add a follow-up to do something more proper.

}
} else {
for (String audience : accessToken.getAudience()) {
ClientModel client = session.clients().getClientByClientId(session.getContext().getRealm(), audience);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recall we discussed also the option when aud claim in the token would contain directly the URL itself (instead of clientId). So in that case, we will possibly need something like "lookup client by resource-url" in addition getClientByClientId . But maybe something as a follow-up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so it supports either urn:client:myclient or https://theclientendpoint now. This should be a bit more clear in the changes I'm about to push

.collect(Collectors.toSet()));
}

session.getAllProviders(TokenInterceptorProvider.class).forEach(i -> i.intercept(accessToken, clientSessionCtx));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TokenInterceptorProvider currently has method signature lik:

AccessToken intercept(AccessToken token, ClientSessionContext clientSessionCtx);

So it is supposed to return the AccessToken . However it seems this invocation ignores the returned value and rather always assumes that original AccessToken instance was updated.

Would be better to either replace accessToken with returned accessToken or eventually change the signature of TokenInterceptorProvider to return void to make it more clear that interceptor can update existing accessToken instance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this around quite a bit and it's now invoked in TokenManager#build

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the way that the TokenInterceptor is currently applied, it seems to function as some kind of token "post processing" where we can modify the current state of a token before it is encoded.

When I initially read the term TokenInterceptor I thought this would allow to pre/postprocess or replace a token.

Perhaps a name like TokenPostProcessor would be closer to the actual usage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is probably a better name. Will rename it.

While doing this I realised that it's a bit of a PITA to add this kinda stuff; and there's code all over the place for different OAuth capabilities. Might be good to at some point introduce some sort of OAuthGrantInterceptor that can expose params, handle params, update tokens, etc. for all different grants. Then in theory a lot of the stuff that is repeated all over the place could be centralized per capability/param.

@stianst stianst force-pushed the resource-indicators-poc branch 2 times, most recently from 5737b35 to e87f184 Compare March 11, 2026 08:36
@stianst stianst changed the title PoC support for resource indicators Initial experimental support for Resource Indicators Mar 11, 2026
@stianst stianst linked an issue Mar 11, 2026 that may be closed by this pull request
@stianst stianst marked this pull request as ready for review March 11, 2026 08:53
@stianst stianst requested a review from a team as a code owner March 11, 2026 08:53
@stianst stianst requested a review from mposolda March 11, 2026 14:21
stianst added 2 commits March 12, 2026 09:28
Closes keycloak#47040

Signed-off-by: stianst <[email protected]>

# Conflicts:
#	services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
#	tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java
@stianst stianst force-pushed the resource-indicators-poc branch from a9b2ad0 to 4308fc7 Compare March 12, 2026 08:28
Copy link
Copy Markdown
Contributor

@rmartinc rmartinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @stianst! For me it's OK as starting point. But I would say that we need the following follow-ups:

  • We need to check the resource parameter is OK. The must checks in the spec (absolute, no fragments,...). We can extend with client policies if needed for custom checks or the should ones.
  • Extend for several resources instead of one.
  • Changes in the admin UI to include the new attributes.

With this PR, we don't need any boolean in the requester client to say that the client wants to use resource indicators. It just uses resource syntax if the resource parameter is included in the request. If no resource param, it acts as before. For me it's OK but I understood that we didn't want this in the meetings.

}

String requestedResource = clientSessionCtx.getAttribute(OAuth2Constants.RESOURCE, String.class);
if (requestedResource == null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the config option to enable the resource at client level should be checked here in the first PR. Note that if enabled, all URLs should be returned for the client if the resource passed is null, or some default URLs at least, not null.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the token has some audience and at least one of the audience has a resource url set, that would be the default.

Added a follow-up to revisit this more properly in #47126, but since I'm not 100% sure how that should look like I'd rather leave it as is for now, and revisit in the #47126


protected TokenManager.AccessTokenResponseBuilder createTokenResponseBuilder(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx, String scopeParam, Function<TokenManager.AccessTokenResponseBuilder, ClientPolicyContext> clientPolicyContextGenerator) {
clientSessionCtx.setAttribute(Constants.GRANT_TYPE, context.getGrantType());
clientSessionCtx.setAttribute(OAuth2Constants.RESOURCE, formParams.getFirst(OAuth2Constants.RESOURCE));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we need to check the resource parameter following the spec.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to find a way to verify the resource parameter properly, but it's not straightforward as there are multiple places that will set the resource parameter as well as I'd like to extract as much as possible into a separate provider. I'd like to handle this as a follow-up in #47116


private String findAudienceByClientUrn(String resource, String[] audience) {
String requestedClientId = resource.substring(URN_CLIENT_PREFIX.length());
return find(requestedClientId, audience);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stianst This means when resource urn:client:xxxx the audience is just set to xxxx, right? yes, I see the same in the tests. For me it's OK although it surprised me initially.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so the thinking was that urn:client was a special case to support filtering audiences as is. We pretty much know that no clients will be expecting the special urn:client: in the audience, but at the same time it needs to be a URI in the resource parameter. Will make sure to document this as I agree it may be a bit unexpected. Added a note to #47127 to make sure it's covered in the docs.

@stianst
Copy link
Copy Markdown
Contributor Author

stianst commented Mar 13, 2026

Thanks @stianst! For me it's OK as starting point. But I would say that we need the following follow-ups:

  • We need to check the resource parameter is OK. The must checks in the spec (absolute, no fragments,...). We can extend with client policies if needed for custom checks or the should ones.

#47116

  • Extend for several resources instead of one.

#47128

  • Changes in the admin UI to include the new attributes.

#47123

With this PR, we don't need any boolean in the requester client to say that the client wants to use resource indicators. It just uses resource syntax if the resource parameter is included in the request. If no resource param, it acts as before. For me it's OK but I understood that we didn't want this in the meetings.

I think we want to be explicit about enabling resource indicators for RPs, but I've added this as a follow-up to #47121 - it may also not be needed in the end as I'm not sure there is any harm in enabling resource params always, but that may depend a bit on what we do in #47126

Copy link
Copy Markdown
Contributor

@rmartinc rmartinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @stianst! Approved!

Copy link
Copy Markdown
Contributor

@mposolda mposolda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates. Figured just one additional issue (Up to you if you prefer to add this as a follow-up or directly in this PR).


String grantType = context.clientSessionCtx().getAttribute(Constants.GRANT_TYPE, String.class);

if (grantType.equals(OAuth2Constants.REFRESH_TOKEN)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the refreshToken request, there is the possible issue that originalAudience from refresh token is not propagated back to the newly issued access-token in this PR.

As for example when accessToken will have multiple audiences after processing of protocol mappers (EG. theservice and theservice2), then the refreshed access token will still contain both of these audiences instead of the "original audience" from refresh-token. This is not the desired behaviour IMO?

Just a note that this issue is not visible in your automated tests as they use single audience theservice to be in the access-token after the protocol mappers processing, so it seems it is not tested that some undesired audiences should be filtered from the access token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't initially planning on including refresh token request in this PR at all, but ended up adding it, but it's not completed. I forgot to add a follow-up task around this and added #47180 now to capture this case.

Would it be okay to handle it as a follow-up?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, follow-up is ok

Copy link
Copy Markdown
Contributor

@mposolda mposolda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the PR, considering that we have follow-up issues to handle remaining stuff

@stianst stianst merged commit ca2bc8b into keycloak:main Mar 17, 2026
84 checks passed
@stianst stianst deleted the resource-indicators-poc branch March 17, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Initial experimental support for Resource Indicators

4 participants