Better events for jwt-bearer and check all details in the tests

CLoses #44137

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2025-11-25 11:51:49 +01:00 committed by Marek Posolda
parent 2210b1ed50
commit d0e4d1f620
7 changed files with 71 additions and 33 deletions

View File

@ -39,7 +39,9 @@ public interface Details {
String AUTH_TYPE = "auth_type";
String AUTH_METHOD = "auth_method";
String IDENTITY_PROVIDER = "identity_provider";
String IDENTITY_PROVIDER_ISSUER = "identity_provider_issuer";
String IDENTITY_PROVIDER_USERNAME = "identity_provider_identity";
String IDENTITY_PROVIDER_USER_ID = "identity_provider_user_id";
String IDENTITY_PROVIDER_BROKER_SESSION_ID = "identity_provider_broker_session_id";
String REGISTER_METHOD = "register_method";
String USERNAME = "username";

View File

@ -58,6 +58,8 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
JWTAuthorizationGrantValidator authorizationGrantContext = JWTAuthorizationGrantValidator.createValidator(
context.getSession(), client, assertion, formParams.getFirst(OAuth2Constants.SCOPE));
event.detail(Details.IDENTITY_PROVIDER_ISSUER, authorizationGrantContext.getIssuer());
event.detail(Details.IDENTITY_PROVIDER_USER_ID, authorizationGrantContext.getSubject());
//client must be confidential
authorizationGrantContext.validateClient();
@ -73,6 +75,7 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
if (identityProviderModel == null) {
throw new RuntimeException("No Identity Provider for provided issuer");
}
event.detail(Details.IDENTITY_PROVIDER, identityProviderModel.getAlias());
if(!OIDCAdvancedConfigWrapper.fromClientModel(context.getClient()).getJWTAuthorizationGrantAllowedIdentityProviders().contains(identityProviderModel.getAlias())) {
throw new RuntimeException("Identity Provider is not allowed for the client");

View File

@ -42,6 +42,16 @@ public class EventAssertion {
return this;
}
public EventAssertion sessionId(String sessionId) {
Assertions.assertEquals(sessionId, event.getSessionId());
return this;
}
public EventAssertion userId(String userId) {
Assertions.assertEquals(userId, event.getUserId());
return this;
}
public EventAssertion details(String key, String value) {
if (value != null) {
MatcherAssert.assertThat(event.getDetails(), Matchers.hasEntry(key, value));

View File

@ -6,9 +6,11 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.testframework.events.EventAssertion;
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
@ -83,7 +85,7 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 5L));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
assertSuccess("test-app", response);
//test with iat
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 20L, (long) Time.currentTime()));
@ -93,7 +95,7 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 20L, (long) Time.currentTime()));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
assertSuccess("test-app", response);
}
@Test
@ -109,12 +111,12 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
// 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);
assertSuccess("test-app", 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);
assertSuccess("test-app", response);
// Introspection endpoint as audience does not work
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIntrospection(), IDP_ISSUER));
@ -144,7 +146,8 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), "fake-issuer", Time.currentTime() + 300L));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("No Identity Provider for provided issuer", response, events.poll());
EventAssertion event = assertFailure("No Identity Provider for provided issuer", response, events.poll());
event.details(Details.IDENTITY_PROVIDER_ISSUER, "fake-issuer");
}
@Test
@ -155,14 +158,17 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("fake-user", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("User not found", response, events.poll());
EventAssertion event = assertFailure("User not found", response, events.poll());
event.details(Details.IDENTITY_PROVIDER, IDP_ALIAS);
event.details(Details.IDENTITY_PROVIDER_ISSUER, IDP_ISSUER);
event.details(Details.IDENTITY_PROVIDER_USER_ID, "fake-user");
}
@Test
public void testReplayToken() {
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
assertSuccess("test-app", response);
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Token reuse detected", response, events.poll());
@ -173,10 +179,10 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
assertSuccess("test-app", response);
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
assertSuccess("test-app", response);
}
@Test
@ -186,7 +192,7 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
});
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
assertSuccess("test-app", response);
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG, Algorithm.ES512);
@ -213,7 +219,7 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
try {
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
AccessToken token = assertSuccess("test-app", "basic-user", response);
AccessToken token = assertSuccess("test-app", response);
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder(new String[]{"email", "profile", "address", "phone"}));
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
@ -229,6 +235,6 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
public void testSuccessGrant() {
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
assertSuccess("test-app", response);
}
}

View File

@ -21,6 +21,7 @@ package org.keycloak.tests.oauth;
import java.util.UUID;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
@ -74,7 +75,7 @@ public class BaseAbstractJWTAuthorizationGrantTest {
protected ManagedRealm realm;
@InjectUser(config = AbstractJWTAuthorizationGrantTest.FederatedUserConfiguration.class)
ManagedUser user;
protected ManagedUser user;
@InjectOAuthClient
OAuthClient oAuthClient;
@ -161,47 +162,58 @@ public class BaseAbstractJWTAuthorizationGrantTest {
}
}
protected AccessToken assertSuccess(String expectedClientId, String username, AccessTokenResponse response) {
protected AccessToken assertSuccess(String expectedClientId, AccessTokenResponse response) {
Assertions.assertTrue(response.isSuccess());
Assertions.assertNull(response.getRefreshToken());
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
Assertions.assertNull(accessToken.getSessionId());
MatcherAssert.assertThat(accessToken.getId(), Matchers.startsWith("trrtag:"));
Assertions.assertEquals(expectedClientId, accessToken.getIssuedFor());
Assertions.assertEquals(username, accessToken.getPreferredUsername());
Assertions.assertEquals(user.getUsername(), accessToken.getPreferredUsername());
EventAssertion.assertSuccess(events.poll())
.type(EventType.LOGIN)
.clientId(expectedClientId)
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
.details("username", username);
.sessionId(null)
.userId(user.getId())
.details(Details.GRANT_TYPE, OAuth2Constants.JWT_AUTHORIZATION_GRANT)
.details(Details.IDENTITY_PROVIDER, IDP_ALIAS)
.details(Details.IDENTITY_PROVIDER_ISSUER, IDP_ISSUER)
.details(Details.IDENTITY_PROVIDER_USER_ID, "basic-user-id")
.details(Details.USERNAME, user.getUsername());
return accessToken;
}
protected void assertFailure(String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
assertFailure("invalid_grant", expectedErrorDescription, response, event);
protected EventAssertion assertFailure(String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
return assertFailure(OAuthErrorException.INVALID_GRANT, expectedErrorDescription, response, event);
}
protected void assertFailure(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
protected EventAssertion assertFailure(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
Assertions.assertFalse(response.isSuccess());
Assertions.assertEquals(expectedError, response.getError());
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
EventAssertion.assertError(event)
return EventAssertion.assertError(event)
.type(EventType.LOGIN_ERROR)
.error("invalid_request")
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
.details("reason", expectedErrorDescription);
.sessionId(null)
.error(OAuthErrorException.INVALID_REQUEST)
.details(Details.GRANT_TYPE, OAuth2Constants.JWT_AUTHORIZATION_GRANT)
.details(Details.REASON, expectedErrorDescription);
}
protected void assertFailurePolicy(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
protected EventAssertion assertFailurePolicy(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
Assertions.assertFalse(response.isSuccess());
Assertions.assertEquals(expectedError, response.getError());
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
EventAssertion.assertError(event)
return EventAssertion.assertError(event)
.type(EventType.LOGIN_ERROR)
.sessionId(null)
.userId(user.getId())
.error(expectedError)
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
.details("reason", Details.CLIENT_POLICY_ERROR)
.details(Details.GRANT_TYPE, OAuth2Constants.JWT_AUTHORIZATION_GRANT)
.details(Details.IDENTITY_PROVIDER_ISSUER, IDP_ISSUER)
.details(Details.IDENTITY_PROVIDER_USER_ID, "basic-user-id")
.details(Details.REASON, Details.CLIENT_POLICY_ERROR)
.details(Details.CLIENT_POLICY_ERROR, expectedError)
.details(Details.CLIENT_POLICY_ERROR_DETAIL, expectedErrorDescription);
.details(Details.CLIENT_POLICY_ERROR_DETAIL, expectedErrorDescription)
.details(Details.USERNAME, user.getUsername());
}
}

View File

@ -33,19 +33,19 @@ public class JWTAuthorizationGrantDownscopeClientPoliciesTest extends BaseAbstra
// test with all the scopes
String jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("email profile address"));
AccessTokenResponse response = oAuthClient.openid(false).scope("address").jwtAuthorizationGrantRequest(jwt).send();
AccessToken token = assertSuccess("test-app", "basic-user", response);
AccessToken token = assertSuccess("test-app", response);
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder("email", "profile", "address"));
// test with less scopes => downscope
jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("email profile address"));
response = oAuthClient.openid(false).scope(null).jwtAuthorizationGrantRequest(jwt).send();
token = assertSuccess("test-app", "basic-user", response);
token = assertSuccess("test-app", response);
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder("email", "profile"));
// test default scopes are restricted if not present in initial token
jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("profile address"));
response = oAuthClient.openid(false).scope("address").jwtAuthorizationGrantRequest(jwt).send();
token = assertSuccess("test-app", "basic-user", response);
token = assertSuccess("test-app", response);
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder("profile", "address"));
// test requesting a valid optional scope for the client but not present initially

View File

@ -2,9 +2,11 @@ package org.keycloak.tests.oauth;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.events.Details;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.events.EventAssertion;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmConfigBuilder;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
@ -29,7 +31,10 @@ public class OIDCIdentityProviderJWTAuthorizationGrantTest extends AbstractJWTAu
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER));
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("JWT Authorization Granted is not enabled for the identity provider", response, events.poll());
EventAssertion event = assertFailure("JWT Authorization Granted is not enabled for the identity provider", response, events.poll());
event.details(Details.IDENTITY_PROVIDER, IDP_ALIAS);
event.details(Details.IDENTITY_PROVIDER_ISSUER, IDP_ISSUER);
event.details(Details.IDENTITY_PROVIDER_USER_ID, "basic-user-id");
}
@Test