Audience validation according to latest specs proposal

closes #43984

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2025-11-05 18:03:19 +01:00 committed by Marek Posolda
parent 6043027d99
commit b8a8be33aa
5 changed files with 56 additions and 4 deletions

View File

@ -15,6 +15,8 @@
* limitations under the License.
*/
package org.keycloak.broker.provider;
import java.util.List;
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
public interface JWTAuthorizationGrantProvider {
@ -25,4 +27,9 @@ public interface JWTAuthorizationGrantProvider {
boolean isAssertionReuseAllowed();
/**
* @return list of allowed audience values. JWT assertion is considered valid if it's audience is one of the audiences returned from this method
*/
List<String> getAllowedAudienceForJWTGrant();
}

View File

@ -75,6 +75,7 @@ import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.IdentityBrokerService;
@ -86,6 +87,7 @@ import org.keycloak.util.TokenUtil;
import org.keycloak.vault.VaultStringSecret;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@ -1096,4 +1098,14 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
public boolean isAssertionReuseAllowed() {
return getConfig().getJwtAuthorizationGrantAssertionReuseAllowed();
}
@Override
public List<String> getAllowedAudienceForJWTGrant() {
RealmModel realm = session.getContext().getRealm();
URI baseUri = session.getContext().getUri().getBaseUri();
String issuer = Urls.realmIssuer(baseUri, realm.getName());
String tokenEndpoint = Urls.tokenEndpoint(baseUri, realm.getName()).toString();
return List.of(issuer, tokenEndpoint);
}
}

View File

@ -44,7 +44,6 @@ import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import jakarta.ws.rs.core.Response;
import java.util.List;
public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
@ -53,7 +52,6 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
setContext(context);
String assertion = formParams.getFirst(OAuth2Constants.ASSERTION);
String expectedAudience = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName());
try {
@ -64,7 +62,6 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
authorizationGrantContext.validateClient();
//mandatory claims
authorizationGrantContext.validateTokenAudience(List.of(expectedAudience), false);
authorizationGrantContext.validateIssuer();
authorizationGrantContext.validateSubject();
@ -88,6 +85,9 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
// assign the provider and perform validations associated to the jwt grant provider
authorizationGrantContext.validateTokenActive(jwtAuthorizationGrantProvider.getAllowedClockSkew(), 300, jwtAuthorizationGrantProvider.isAssertionReuseAllowed());
// Validate audience
authorizationGrantContext.validateTokenAudience(jwtAuthorizationGrantProvider.getAllowedAudienceForJWTGrant(), false);
//validate the JWT assertion and get the brokered identity from the idp
BrokeredIdentityContext brokeredIdentityContext = jwtAuthorizationGrantProvider.validateAuthorizationGrantAssertion(authorizationGrantContext);
if (brokeredIdentityContext == null) {

View File

@ -184,6 +184,10 @@ public class Urls {
return tokenBase(baseUri).path(OIDCLoginProtocolService.class, "logout");
}
public static URI tokenEndpoint(URI baseUri, String realmName) {
return tokenBase(baseUri).path(OIDCLoginProtocolService.class, "token").build(realmName);
}
public static URI realmRegisterAction(URI baseUri, String realmName) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "processRegister").build(realmName);
}

View File

@ -125,7 +125,7 @@ public class JWTAuthorizationGrantTest {
}
@Test
public void testBadAudience() {
public void testAudience() {
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", null, IDP_ISSUER, Time.currentTime() + 300L));
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Invalid token audience", response, events.poll());
@ -133,6 +133,35 @@ public class JWTAuthorizationGrantTest {
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", "fake-audience", IDP_ISSUER));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Invalid token audience", response, events.poll());
// Issuer as audience works
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
// Token endpoint as audience works
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getToken(), IDP_ISSUER));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
// Introspection endpoint as audience does not work
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIntrospection(), IDP_ISSUER));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Invalid token audience", response, events.poll());
// Multiple audiences does not work
JsonWebToken jwtToken = createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER);
jwtToken.addAudience("fake");
jwt = getIdentityProvider().encodeToken(jwtToken);
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Multiple audiences not allowed", response, events.poll());
// Multiple audiences does not work (even if both are valid)
jwtToken = createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER);
jwtToken.addAudience(oAuthClient.getEndpoints().getToken());
jwt = getIdentityProvider().encodeToken(jwtToken);
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Multiple audiences not allowed", response, events.poll());
}
@Test