Refactor and improve tests for federated client authentication (#42720)

Closes #42718

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2025-09-18 11:30:01 +02:00 committed by GitHub
parent 2d34ebe33e
commit 37a99154a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 343 additions and 265 deletions

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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