diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractJWTClientValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractJWTClientValidator.java index 4af380b83b6..96b2dbd32ad 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractJWTClientValidator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractJWTClientValidator.java @@ -117,13 +117,12 @@ public abstract class AbstractJWTClientValidator { return false; } - context.getEvent().client(clientId); - client = clientAssertionState.getClient(); if (client == null) { return failure(AuthenticationFlowError.CLIENT_NOT_FOUND); } else { + context.getEvent().client(client.getClientId()); context.setClient(client); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/events/EventAssertion.java b/test-framework/core/src/main/java/org/keycloak/testframework/events/EventAssertion.java new file mode 100644 index 00000000000..83cc2ef9ec5 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/events/EventAssertion.java @@ -0,0 +1,60 @@ +package org.keycloak.testframework.events; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.keycloak.events.EventType; +import org.keycloak.representations.idm.EventRepresentation; + +public class EventAssertion { + + private final EventRepresentation event; + + protected EventAssertion(EventRepresentation event) { + Assertions.assertNotNull(event, "Event was null"); + Assertions.assertNotNull(event.getId(), "Event id was null"); + this.event = event; + } + + public static EventAssertion assertSuccess(EventRepresentation event) { + Assertions.assertFalse(event.getType().endsWith("_ERROR"), "Expected successful event"); + return new EventAssertion(event); + } + + public static EventAssertion assertError(EventRepresentation event) { + Assertions.assertTrue(event.getType().endsWith("_ERROR"), "Expected error event"); + return new EventAssertion(event); + } + + public EventAssertion error(String error) { + Assertions.assertEquals(error, event.getError()); + return this; + } + + public EventAssertion type(EventType type) { + Assertions.assertEquals(type, EventType.valueOf(event.getType())); + return this; + } + + public EventAssertion clientId(String clientId) { + Assertions.assertEquals(clientId, event.getClientId()); + return this; + } + + public EventAssertion details(String key, String value) { + if (value != null) { + MatcherAssert.assertThat(event.getDetails(), Matchers.hasEntry(key, value)); + } else { + withoutDetails(key); + } + return this; + } + + public EventAssertion withoutDetails(String... keys) { + for (String key : keys) { + MatcherAssert.assertThat(event.getDetails(), Matchers.not(Matchers.hasKey(key))); + } + return this; + } + +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ManagedRealm.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ManagedRealm.java index fb33d6642b0..857be6676fb 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ManagedRealm.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ManagedRealm.java @@ -1,6 +1,8 @@ package org.keycloak.testframework.realm; +import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testframework.injection.ManagedTestResource; @@ -56,6 +58,17 @@ public class ManagedRealm extends ManagedTestResource { admin().update(configBuilder.build()); } + public void updateIdentityProviderWithCleanup(String alias, IdentityProviderUpdate update) { + IdentityProviderResource resource = realmResource.identityProviders().get(alias); + + IdentityProviderRepresentation original = resource.toRepresentation(); + IdentityProviderRepresentation updated = RepresentationUtils.clone(original); + update.update(updated); + resource.update(updated); + + cleanup().add(r -> r.identityProviders().get(alias).update(original)); + } + public ManagedRealmCleanup cleanup() { if (cleanup == null) { cleanup = new ManagedRealmCleanup(); @@ -77,4 +90,10 @@ public class ManagedRealm extends ManagedTestResource { } + public interface IdentityProviderUpdate { + + void update(IdentityProviderRepresentation rep); + + } + } diff --git a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/AbstractFederatedClientAuthTest.java b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/AbstractFederatedClientAuthTest.java new file mode 100644 index 00000000000..66274f2873c --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/AbstractFederatedClientAuthTest.java @@ -0,0 +1,179 @@ +package org.keycloak.tests.client.authentication.external; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.common.util.Time; +import org.keycloak.events.EventType; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.testframework.annotations.InjectEvents; +import org.keycloak.testframework.events.EventAssertion; +import org.keycloak.testframework.events.Events; +import org.keycloak.testframework.oauth.OAuthClient; +import org.keycloak.testframework.oauth.OAuthIdentityProvider; +import org.keycloak.testframework.oauth.annotations.InjectOAuthClient; +import org.keycloak.testsuite.util.oauth.AccessTokenResponse; + +public abstract class AbstractFederatedClientAuthTest { + + private final String expectedTokenIssuer; + private final String internalClientId; + private final String externalClientId; + + @InjectOAuthClient + OAuthClient oAuthClient; + + @InjectEvents + Events events; + + public AbstractFederatedClientAuthTest(String expectedTokenIssuer, String internalClientId, String externalClientId) { + this.expectedTokenIssuer = expectedTokenIssuer; + this.internalClientId = internalClientId; + this.externalClientId = externalClientId; + } + + @Test + public void testValidToken() { + JsonWebToken token = createDefaultToken(); + assertSuccess(internalClientId, doClientGrant(token)); + assertSuccess(internalClientId, token.getId(), expectedTokenIssuer, externalClientId, events.poll()); + } + + @Test + public void testInvalidSignature() { + OAuthIdentityProvider.OAuthIdentityProviderKeys keys = getIdentityProvider().createKeys(); + JsonWebToken jwt = createDefaultToken(); + String jws = getIdentityProvider().encodeToken(jwt, keys); + assertFailure("Invalid client or Invalid client credentials", doClientGrant(jws)); + assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll()); + } + + @Test + public void testInvalidSub() { + JsonWebToken jwt = createDefaultToken(); + jwt.subject("invalid"); + Assertions.assertFalse(doClientGrant(jwt).isSuccess()); + assertFailure(null, expectedTokenIssuer, "invalid", jwt.getId(), "client_not_found", events.poll()); + } + + @Test + public void testExpired() { + JsonWebToken jwt = createDefaultToken(); + jwt.exp((long) (Time.currentTime() - 30)); + assertFailure("Token is not active", doClientGrant(jwt)); + assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll()); + } + + @Test + public void testMissingExp() { + JsonWebToken jwt = createDefaultToken(); + jwt.exp(null); + assertFailure("Token exp claim is required", doClientGrant(jwt)); + assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll()); + } + + @Test + public void testInvalidNbf() { + JsonWebToken jwt = createDefaultToken(); + jwt.nbf((long) (Time.currentTime() + 60)); + assertFailure("Token is not active", doClientGrant(jwt)); + assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll()); + } + + @Test + public void testInvalidAud() { + JsonWebToken jwt = createDefaultToken(); + jwt.audience("invalid"); + assertFailure("Invalid token audience", doClientGrant(jwt)); + assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll()); + } + + @Test + public void testMissingAud() { + JsonWebToken jwt = createDefaultToken(); + jwt.audience((String) null); + assertFailure("Invalid token audience", doClientGrant(jwt)); + assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll()); + } + + @Test + public void testMultipleAud() { + JsonWebToken jwt = createDefaultToken(); + jwt.audience(jwt.getAudience()[0], "invalid"); + assertFailure("Multiple audiences not allowed", doClientGrant(jwt)); + assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll()); + } + + @Test + public void testValidInvalidAssertionType() { + JsonWebToken jwt = createDefaultToken(); + String jws = getIdentityProvider().encodeToken(jwt); + AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, "urn:ietf:params:oauth:client-assertion-type:invalid").send(); + assertFailure(response); + assertFailure(null, expectedTokenIssuer, externalClientId, jwt.getId(), "client_not_found", events.poll()); + } + + protected abstract OAuthIdentityProvider getIdentityProvider(); + + protected abstract JsonWebToken createDefaultToken(); + + protected AccessTokenResponse doClientGrant(JsonWebToken token) { + String jws = getIdentityProvider().encodeToken(token); + return doClientGrant(jws); + } + + protected AccessTokenResponse doClientGrant(String jws) { + AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, getClientAssertionType()).send(); + return response; + } + + protected void assertSuccess(String expectedClientId, AccessTokenResponse response) { + Assertions.assertTrue(response.isSuccess()); + AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class); + Assertions.assertEquals(expectedClientId, accessToken.getIssuedFor()); + } + + protected void assertSuccess(String expectedClientId, String expectedAssertionId, String expectedAssertionIssuer, String expectedAssertionSub, EventRepresentation event) { + EventAssertion.assertSuccess(event) + .type(EventType.CLIENT_LOGIN) + .clientId(expectedClientId) + .details("client_assertion_id", expectedAssertionId) + .details("client_assertion_issuer", expectedAssertionIssuer) + .details("client_assertion_sub", expectedAssertionSub) + .details("client_auth_method", "federated-jwt") + .details("grant_type", "client_credentials") + .details("username", "service-account-" + expectedClientId); + } + + protected void assertFailure(AccessTokenResponse response) { + assertFailure("Invalid client or Invalid client credentials", response); + } + + protected void assertFailure(String expectedErrorDescription, AccessTokenResponse response) { + Assertions.assertFalse(response.isSuccess()); + Assertions.assertEquals("invalid_client", response.getError()); + Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription()); + } + + protected void assertFailure(String expectedClientId, String expectedAssertionIssuer, String expectedAssertionSub, String expectedAssertionId, EventRepresentation event) { + assertFailure(expectedClientId, expectedAssertionIssuer, expectedAssertionSub, expectedAssertionId, "invalid_client_credentials", event); + } + + protected void assertFailure(String expectedClientId, String expectedAssertionIssuer, String expectedAssertionSub, String expectedAssertionId, String expectedError, EventRepresentation event) { + EventAssertion.assertError(event) + .type(EventType.CLIENT_LOGIN_ERROR) + .clientId(expectedClientId) + .error(expectedError) + .details("client_assertion_id", expectedAssertionId) + .details("client_assertion_issuer", expectedAssertionIssuer) + .details("client_assertion_sub", expectedAssertionSub) + .details("grant_type", "client_credentials"); + } + + protected String getClientAssertionType() { + return OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT; + } + +} diff --git a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthFromKeycloakTest.java b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthFromKeycloakTest.java index 464f29c20d6..63ca309620d 100644 --- a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthFromKeycloakTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthFromKeycloakTest.java @@ -31,7 +31,7 @@ public class FederatedClientAuthFromKeycloakTest { @InjectRealm(ref = "external") ManagedRealm externalRealm; - @InjectOAuthClient(config = InternalClientConfig.class) + @InjectOAuthClient OAuthClient internalOAuthClient; @InjectOAuthClient(ref = "external", realmRef = "external", config = ExternalClientConfig.class) @@ -49,7 +49,7 @@ public class FederatedClientAuthFromKeycloakTest { @Override public RealmConfigBuilder configure(RealmConfigBuilder realm) { - return realm.identityProvider( + realm.identityProvider( IdentityProviderBuilder.create() .providerId(OIDCIdentityProviderFactory.PROVIDER_ID) .alias(IDP_ALIAS) @@ -59,18 +59,14 @@ public class FederatedClientAuthFromKeycloakTest { .setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://localhost:8080/realms/external/protocol/openid-connect/certs") .setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, "true") .build()); - } - } - public static class InternalClientConfig implements ClientConfig { - - @Override - public ClientConfigBuilder configure(ClientConfigBuilder client) { - return client.clientId("myclient") + realm.addClient("myclient") .serviceAccountsEnabled(true) .authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID) .attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS) .attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, "myclient"); + + return realm; } } diff --git a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthTest.java b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthTest.java index d584cc3cde5..83b25cfc7ad 100644 --- a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthTest.java @@ -2,185 +2,105 @@ package org.keycloak.tests.client.authentication.external; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.common.util.Time; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.JsonWebToken; -import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.testframework.annotations.InjectClient; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; import org.keycloak.testframework.oauth.OAuthIdentityProvider; -import org.keycloak.testframework.oauth.OAuthClient; import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider; -import org.keycloak.testframework.oauth.annotations.InjectOAuthClient; -import org.keycloak.testframework.realm.ClientConfig; -import org.keycloak.testframework.realm.ClientConfigBuilder; -import org.keycloak.testframework.realm.ManagedClient; import org.keycloak.testframework.realm.ManagedRealm; import org.keycloak.testframework.realm.RealmConfig; import org.keycloak.testframework.realm.RealmConfigBuilder; import org.keycloak.testsuite.util.IdentityProviderBuilder; -import org.keycloak.testsuite.util.oauth.AccessTokenResponse; import java.util.UUID; @KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class) -public class FederatedClientAuthTest { +public class FederatedClientAuthTest extends AbstractFederatedClientAuthTest { private static final String IDP_ALIAS = "external-idp"; + private static final String TOKEN_ISSUER = "http://127.0.0.1:8500"; private static final String INTERNAL_CLIENT_ID = "internal-myclient"; private static final String EXTERNAL_CLIENT_ID = "external-myclient"; @InjectRealm(config = ExernalClientAuthRealmConfig.class) protected ManagedRealm realm; - @InjectClient(config = ExernalClientAuthClientConfig.class) - protected ManagedClient client; - - @InjectOAuthClient - OAuthClient oAuthClient; - @InjectOAuthIdentityProvider OAuthIdentityProvider identityProvider; - @Test - public void testInvalidSignature() { - OAuthIdentityProvider.OAuthIdentityProviderKeys keys = identityProvider.createKeys(); - String jws = identityProvider.encodeToken(createDefaultToken(), keys); - Assertions.assertFalse(doClientGrant(jws)); - } - - @Test - public void testInvalidAssertionType() { - String jws = identityProvider.encodeToken(createDefaultToken()); - AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe").send(); - Assertions.assertFalse(response.isSuccess()); - } - - @Test - public void testValidToken() { - Assertions.assertTrue(doClientGrant(createDefaultToken())); + public FederatedClientAuthTest() { + super(TOKEN_ISSUER, INTERNAL_CLIENT_ID, EXTERNAL_CLIENT_ID); } @Test public void testInvalidIssuer() { - JsonWebToken token = createDefaultToken(); - token.issuer("http://invalid"); - Assertions.assertFalse(doClientGrant(token)); + JsonWebToken jwt = createDefaultToken(); + jwt.issuer("http://invalid"); + + assertFailure("Invalid client or Invalid client credentials", doClientGrant(jwt)); + assertFailure(null, "http://invalid", jwt.getSubject(), jwt.getId(), "client_not_found", events.poll()); } @Test public void testMissingIssuer() { - JsonWebToken token = createDefaultToken(); - token.issuer(null); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testInvalidSub() { - JsonWebToken token = createDefaultToken(); - token.subject("invalid"); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testInvalidAud() { - JsonWebToken token = createDefaultToken(); - token.audience("invalid"); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testMultipleAud() { - JsonWebToken token = createDefaultToken(); - token.audience(token.getAudience()[0], "invalid"); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testInvalidNbf() { - JsonWebToken token = createDefaultToken(); - token.nbf((long) (Time.currentTime() + 30)); - Assertions.assertFalse(doClientGrant(token)); + JsonWebToken jwt = createDefaultToken(); + jwt.issuer(null); + Assertions.assertFalse(doClientGrant(jwt).isSuccess()); + assertFailure(null, null, jwt.getSubject(), jwt.getId(), "client_not_found", events.poll()); } @Test public void testMissingJti() { - JsonWebToken token = createDefaultToken(); - token.id(null); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testExpired() { - JsonWebToken token = createDefaultToken(); - token.exp((long) (Time.currentTime() - 30)); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testMissingExp() { - JsonWebToken token = createDefaultToken(); - token.exp(null); - Assertions.assertFalse(doClientGrant(token)); + JsonWebToken jwt = createDefaultToken(); + jwt.id(null); + Assertions.assertFalse(doClientGrant(jwt).isSuccess()); + assertFailure(INTERNAL_CLIENT_ID, TOKEN_ISSUER, jwt.getSubject(), jwt.getId(), events.poll()); } @Test public void testReuseNotPermitted() { - JsonWebToken token = createDefaultToken(); - Assertions.assertTrue(doClientGrant(token)); - Assertions.assertFalse(doClientGrant(token)); + JsonWebToken jwt = createDefaultToken(); + assertSuccess(INTERNAL_CLIENT_ID, doClientGrant(jwt)); + assertSuccess(INTERNAL_CLIENT_ID, jwt.getId(), TOKEN_ISSUER, EXTERNAL_CLIENT_ID, events.poll()); + assertFailure("Token reuse detected", doClientGrant(jwt)); + assertFailure(INTERNAL_CLIENT_ID, TOKEN_ISSUER, EXTERNAL_CLIENT_ID, jwt.getId(), events.poll()); } @Test public void testReusePermitted() { - IdentityProviderResource idp = realm.admin().identityProviders().get(IDP_ALIAS); - IdentityProviderRepresentation rep = idp.toRepresentation(); - rep.getConfig().put(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE, "true"); - idp.update(rep); + realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> { + rep.getConfig().put(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE, "true"); + }); - try { - JsonWebToken token = createDefaultToken(); - Assertions.assertTrue(doClientGrant(token)); - } finally { - rep.getConfig().remove(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE); - idp.update(rep); - } + JsonWebToken jwt = createDefaultToken(); + assertSuccess(INTERNAL_CLIENT_ID, doClientGrant(jwt)); + assertSuccess(INTERNAL_CLIENT_ID, jwt.getId(), TOKEN_ISSUER, EXTERNAL_CLIENT_ID, events.poll()); + assertSuccess(INTERNAL_CLIENT_ID, doClientGrant(jwt)); + assertSuccess(INTERNAL_CLIENT_ID, jwt.getId(), TOKEN_ISSUER, EXTERNAL_CLIENT_ID, events.poll()); } @Test public void testClientAssertionsNotSupported() { - IdentityProviderResource idp = realm.admin().identityProviders().get(IDP_ALIAS); - IdentityProviderRepresentation rep = idp.toRepresentation(); - rep.getConfig().remove(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS); - idp.update(rep); + realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> { + rep.getConfig().remove(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS); + }); - Assertions.assertFalse(doClientGrant(createDefaultToken())); - - rep.getConfig().put(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS, "true"); - idp.update(rep); + JsonWebToken jwt = createDefaultToken(); + assertFailure(doClientGrant(jwt)); + assertFailure(null, TOKEN_ISSUER, EXTERNAL_CLIENT_ID, jwt.getId(), "client_not_found", events.poll()); } - private boolean doClientGrant(JsonWebToken token) { - String jws = identityProvider.encodeToken(token); - return doClientGrant(jws); + @Override + protected OAuthIdentityProvider getIdentityProvider() { + return identityProvider; } - private boolean doClientGrant(String jws) { - AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws).send(); - if (response.isSuccess()) { - AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class); - Assertions.assertEquals(INTERNAL_CLIENT_ID, accessToken.getIssuedFor()); - } - return response.isSuccess(); - } - - private JsonWebToken createDefaultToken() { + protected JsonWebToken createDefaultToken() { JsonWebToken token = new JsonWebToken(); token.id(UUID.randomUUID().toString()); token.issuer("http://127.0.0.1:8500"); @@ -195,7 +115,7 @@ public class FederatedClientAuthTest { @Override public RealmConfigBuilder configure(RealmConfigBuilder realm) { - return realm.identityProvider( + realm.identityProvider( IdentityProviderBuilder.create() .providerId(OIDCIdentityProviderFactory.PROVIDER_ID) .alias(IDP_ALIAS) @@ -205,18 +125,14 @@ public class FederatedClientAuthTest { .setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, "true") .setAttribute(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS, "true") .build()); - } - } - public static class ExernalClientAuthClientConfig implements ClientConfig { - - @Override - public ClientConfigBuilder configure(ClientConfigBuilder client) { - return client.clientId(INTERNAL_CLIENT_ID) + realm.addClient(INTERNAL_CLIENT_ID) .serviceAccountsEnabled(true) .authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID) .attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS) .attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, EXTERNAL_CLIENT_ID); + + return realm; } } diff --git a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/IdentityProviderUpdater.java b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/IdentityProviderUpdater.java deleted file mode 100644 index 66920352f52..00000000000 --- a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/IdentityProviderUpdater.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.keycloak.tests.client.authentication.external; - -import org.keycloak.admin.client.resource.IdentityProviderResource; -import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.testframework.realm.ManagedRealm; -import org.keycloak.testframework.realm.RepresentationUtils; - -public class IdentityProviderUpdater { - - public static void updateWithRollback(ManagedRealm realm, String alias, IdentityProviderUpdate update) { - IdentityProviderResource resource = realm.admin().identityProviders().get(alias); - - IdentityProviderRepresentation original = resource.toRepresentation(); - IdentityProviderRepresentation updated = RepresentationUtils.clone(original); - update.update(updated); - resource.update(updated); - - realm.cleanup().add(r -> r.identityProviders().get(alias).update(original)); - } - - public interface IdentityProviderUpdate { - - void update(IdentityProviderRepresentation rep); - - } - -} diff --git a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/SpiffeClientAuthTest.java b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/SpiffeClientAuthTest.java index 8680fb9e744..8cbb7d4fbf3 100644 --- a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/SpiffeClientAuthTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/SpiffeClientAuthTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.keycloak.OAuth2Constants; import org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator; import org.keycloak.broker.spiffe.SpiffeConstants; import org.keycloak.broker.spiffe.SpiffeIdentityProviderConfig; @@ -15,57 +14,36 @@ import org.keycloak.common.util.Time; import org.keycloak.models.IdentityProviderModel; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.testframework.annotations.InjectClient; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; -import org.keycloak.testframework.oauth.OAuthClient; import org.keycloak.testframework.oauth.OAuthIdentityProvider; import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig; import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder; -import org.keycloak.testframework.oauth.annotations.InjectOAuthClient; import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider; -import org.keycloak.testframework.realm.ClientConfig; -import org.keycloak.testframework.realm.ClientConfigBuilder; -import org.keycloak.testframework.realm.ManagedClient; import org.keycloak.testframework.realm.ManagedRealm; import org.keycloak.testframework.realm.RealmConfig; import org.keycloak.testframework.realm.RealmConfigBuilder; import org.keycloak.testframework.server.KeycloakServerConfigBuilder; import org.keycloak.testsuite.util.IdentityProviderBuilder; -import org.keycloak.testsuite.util.oauth.AccessTokenResponse; - -import java.util.UUID; @KeycloakIntegrationTest(config = SpiffeClientAuthTest.SpiffeServerConfig.class) @TestMethodOrder(MethodOrderer.MethodName.class) -public class SpiffeClientAuthTest { +public class SpiffeClientAuthTest extends AbstractFederatedClientAuthTest { + private static final String INTERNAL_CLIENT_ID = "myclient"; + private static final String EXTERNAL_CLIENT_ID = "spiffe://mytrust-domain/myclient"; private static final String IDP_ALIAS = "spiffe-idp"; - - private static final String CLIENT_ID = "spiffe://mytrust-domain/myclient"; + private static final String TRUST_DOMAIN = "spiffe://mytrust-domain"; + private static final String BUNDLE_ENDPOINT = "http://127.0.0.1:8500/idp/jwks"; @InjectRealm(config = ExernalClientAuthRealmConfig.class) protected ManagedRealm realm; - @InjectClient(config = ExernalClientAuthClientConfig.class) - protected ManagedClient client; - - @InjectOAuthClient - OAuthClient oAuthClient; - @InjectOAuthIdentityProvider(config = SpiffeIdpConfig.class) OAuthIdentityProvider identityProvider; - @Test - public void testInvalidSignature() { - OAuthIdentityProvider.OAuthIdentityProviderKeys keys = identityProvider.createKeys(); - String jws = identityProvider.encodeToken(createDefaultToken(), keys); - Assertions.assertFalse(doClientGrant(jws)); - } - - @Test - public void testValidToken() { - Assertions.assertTrue(doClientGrant(createDefaultToken())); + public SpiffeClientAuthTest() { + super(null, INTERNAL_CLIENT_ID, EXTERNAL_CLIENT_ID); } @Test @@ -77,79 +55,36 @@ public class SpiffeClientAuthTest { @Test public void testInvalidTrustDomain() { - IdentityProviderUpdater.updateWithRollback(realm, IDP_ALIAS, rep -> { + realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> { rep.getConfig().put(IdentityProviderModel.ISSUER, "spiffe://different-domain"); }); - Assertions.assertFalse(doClientGrant(createDefaultToken())); - } - - @Test - public void testValidInvalidAssertionType() { - String jws = identityProvider.encodeToken(createDefaultToken()); - AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT).send(); - Assertions.assertFalse(response.isSuccess()); - } - - @Test - public void testInvalidAud() { - JsonWebToken token = createDefaultToken(); - token.audience("invalid"); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testMultipleAud() { - JsonWebToken token = createDefaultToken(); - token.audience(token.getAudience()[0], "invalid"); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testInvalidNbf() { - JsonWebToken token = createDefaultToken(); - token.nbf((long) (Time.currentTime() + 60)); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testExpired() { - JsonWebToken token = createDefaultToken(); - token.exp((long) (Time.currentTime() - 30)); - Assertions.assertFalse(doClientGrant(token)); - } - - @Test - public void testMissingExp() { - JsonWebToken token = createDefaultToken(); - token.exp(null); - Assertions.assertFalse(doClientGrant(token)); + JsonWebToken jwt = createDefaultToken(); + assertFailure(doClientGrant(jwt)); + assertFailure(null, null, jwt.getSubject(), jwt.getId(), "client_not_found", events.poll()); } @Test public void testReuse() { - JsonWebToken token = createDefaultToken(); - token.id(UUID.randomUUID().toString()); - Assertions.assertTrue(doClientGrant(token)); - Assertions.assertTrue(doClientGrant(token)); + JsonWebToken jwt = createDefaultToken(); + assertSuccess(INTERNAL_CLIENT_ID, doClientGrant(jwt)); + assertSuccess(INTERNAL_CLIENT_ID, jwt.getId(), null, EXTERNAL_CLIENT_ID, events.poll()); + assertSuccess(INTERNAL_CLIENT_ID, doClientGrant(jwt)); + assertSuccess(INTERNAL_CLIENT_ID, jwt.getId(), null, EXTERNAL_CLIENT_ID, events.poll()); } - private boolean doClientGrant(JsonWebToken token) { - String jws = identityProvider.encodeToken(token); - return doClientGrant(jws); + @Override + protected OAuthIdentityProvider getIdentityProvider() { + return identityProvider; } - private boolean doClientGrant(String jws) { - AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, SpiffeConstants.CLIENT_ASSERTION_TYPE).send(); - return response.isSuccess(); - } - - private JsonWebToken createDefaultToken() { + @Override + protected JsonWebToken createDefaultToken() { JsonWebToken token = new JsonWebToken(); token.id(null); token.audience(oAuthClient.getEndpoints().getIssuer()); token.exp((long) (Time.currentTime() + 300)); - token.subject(CLIENT_ID); + token.subject(EXTERNAL_CLIENT_ID); return token; } @@ -164,6 +99,11 @@ public class SpiffeClientAuthTest { } } + @Override + protected String getClientAssertionType() { + return SpiffeConstants.CLIENT_ASSERTION_TYPE; + } + public static class SpiffeServerConfig extends ClientAuthIdpServerConfig { @Override @@ -184,25 +124,21 @@ public class SpiffeClientAuthTest { @Override public RealmConfigBuilder configure(RealmConfigBuilder realm) { - return realm.identityProvider( + realm.identityProvider( IdentityProviderBuilder.create() .providerId(SpiffeIdentityProviderFactory.PROVIDER_ID) .alias(IDP_ALIAS) - .setAttribute(IdentityProviderModel.ISSUER, "spiffe://mytrust-domain") - .setAttribute(SpiffeIdentityProviderConfig.BUNDLE_ENDPOINT_KEY, "http://127.0.0.1:8500/idp/jwks") + .setAttribute(IdentityProviderModel.ISSUER, TRUST_DOMAIN) + .setAttribute(SpiffeIdentityProviderConfig.BUNDLE_ENDPOINT_KEY, BUNDLE_ENDPOINT) .build()); - } - } - public static class ExernalClientAuthClientConfig implements ClientConfig { - - @Override - public ClientConfigBuilder configure(ClientConfigBuilder client) { - return client.clientId("myclient") + realm.addClient(INTERNAL_CLIENT_ID) .serviceAccountsEnabled(true) .authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID) .attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS) - .attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, CLIENT_ID); + .attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, EXTERNAL_CLIENT_ID); + + return realm; } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java index 2570299ba11..77f6f80b3e1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -457,7 +457,7 @@ public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest { CloseableHttpResponse resp = sendRequest(oauth.getEndpoints().getToken(), parameters); AccessTokenResponse response = new AccessTokenResponse(resp); - assertError(response,401, "unknown-client", "invalid_client", Errors.CLIENT_NOT_FOUND); + assertError(response,401, null, "invalid_client", Errors.CLIENT_NOT_FOUND); } @Test