mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
Refactor and improve tests for federated client authentication (#42720)
Closes #42718 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
2d34ebe33e
commit
37a99154a5
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user