Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ image:images/spiffe-add-identity-provider.png[Add SPIFFE Provider]
|The SPIFFE Trust domain (for example `spiffe://my-trust-domain`)

|SPIFFE Bundle Endpoint
|`https` URL for the SPIFFE Bundle Endpoint where the SPIFFE servers public keys are exposed
|`https` URL for the SPIFFE Bundle Endpoint or the OpenID Connect JWKS endpoint where the SPIFFE servers public keys are exposed
|===
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ addSamlProvider=Add SAML provider
addSpiffeProvider=Add SPIFFE provider
addKubernetesProvider=Add Kubernetes provider
spiffeTrustDomain=SPIFFE Trust Domain
spiffeBundleEndpoint=SPIFFE Bundle Endpoint
spiffeBundleEndpoint=SPIFFE Bundle or OIDC JWKs endpoint
kubernetesJWKSURL=Kubernetes JWKS URL
kubernetesJWKSURLHelp=Use Kubernetes JWKS URL when accessing an external Kubernetes cluster. The JWKS endpoint must not require authentication
permission=Permission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ public SpiffeBundleEndpointLoader(KeycloakSession session, String bundleEndpoint
@Override
public PublicKeysWrapper loadKeys() throws Exception {
SpiffeJSONWebKeySet jwks = SimpleHttp.create(session).doGet(bundleEndpoint).asJson(SpiffeJSONWebKeySet.class);
PublicKeysWrapper keysWrapper = JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.JWT_SVID);
PublicKeysWrapper keysWrapper = JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.JWT_SVID, true);
if (keysWrapper.getKeys().isEmpty()) {
keysWrapper = JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG, true);
}
return jwks.getSpiffeRefreshHint() == null ? keysWrapper : new PublicKeysWrapper(keysWrapper.getKeys(), jwks.getSpiffeRefreshHint());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ public OAuthIdentityProviderKeys(OAuthIdentityProviderConfigBuilder.OAuthIdentit
if (!config.spiffe()) {
jwk.setAlgorithm("ES256");
}
jwk.setPublicKeyUse(keyUse.getSpecName());
if (config.jwkUse()) {
jwk.setPublicKeyUse(keyUse.getSpecName());
} else {
jwk.setPublicKeyUse(null);
}

Map<String, Object> jwks = new HashMap<>();
jwks.put("keys", new JWK[] { jwk });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
public class OAuthIdentityProviderConfigBuilder {

private boolean spiffe;
private boolean jwkUse = true;

public OAuthIdentityProviderConfigBuilder spiffe() {
spiffe = true;
return this;
}

public OAuthIdentityProviderConfigBuilder jwkUse(boolean jwkUse) {
this.jwkUse = jwkUse;
return this;
}

public OAuthIdentityProviderConfiguration build() {
return new OAuthIdentityProviderConfiguration(spiffe);
return new OAuthIdentityProviderConfiguration(spiffe, jwkUse);
}

public record OAuthIdentityProviderConfiguration(boolean spiffe) {
public record OAuthIdentityProviderConfiguration(boolean spiffe, boolean jwkUse) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.keycloak.tests.client.authentication.external;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.common.util.Time;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;

public abstract class AbstractBaseClientAuthTest extends AbstractClientAuthTest {

public AbstractBaseClientAuthTest(String expectedTokenIssuer, String internalClientId, String externalClientId) {
super(expectedTokenIssuer, internalClientId, 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());
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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;
Expand All @@ -16,105 +14,24 @@
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;

public abstract class AbstractFederatedClientAuthTest {
public abstract class AbstractClientAuthTest {

private final String expectedTokenIssuer;
private final String internalClientId;
private final String externalClientId;
final String expectedTokenIssuer;
final String internalClientId;
final String externalClientId;

@InjectOAuthClient
OAuthClient oAuthClient;

@InjectEvents
Events events;

public AbstractFederatedClientAuthTest(String expectedTokenIssuer, String internalClientId, String externalClientId) {
public AbstractClientAuthTest(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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.util.UUID;

@KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class)
public class FederatedClientAuthTest extends AbstractFederatedClientAuthTest {
public class BaseClientAuthTest extends AbstractBaseClientAuthTest {

private static final String IDP_ALIAS = "external-idp";

Expand All @@ -33,7 +33,7 @@ public class FederatedClientAuthTest extends AbstractFederatedClientAuthTest {
@InjectOAuthIdentityProvider
OAuthIdentityProvider identityProvider;

public FederatedClientAuthTest() {
public BaseClientAuthTest() {
super(TOKEN_ISSUER, INTERNAL_CLIENT_ID, EXTERNAL_CLIENT_ID);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@

@KeycloakIntegrationTest(config = SpiffeClientAuthTest.SpiffeServerConfig.class)
@TestMethodOrder(MethodOrderer.MethodName.class)
public class SpiffeClientAuthTest extends AbstractFederatedClientAuthTest {
public class SpiffeClientAuthTest extends AbstractBaseClientAuthTest {

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 TRUST_DOMAIN = "spiffe://mytrust-domain";
private static final String BUNDLE_ENDPOINT = "http://127.0.0.1:8500/idp/jwks";
static final String INTERNAL_CLIENT_ID = "myclient";
static final String EXTERNAL_CLIENT_ID = "spiffe://mytrust-domain/myclient";
static final String IDP_ALIAS = "spiffe-idp";
static final String TRUST_DOMAIN = "spiffe://mytrust-domain";
static final String BUNDLE_ENDPOINT = "http://127.0.0.1:8500/idp/jwks";

@InjectRealm(config = ExernalClientAuthRealmConfig.class)
protected ManagedRealm realm;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.keycloak.tests.client.authentication.external;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.keycloak.broker.spiffe.SpiffeConstants;
import org.keycloak.common.util.Time;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder;
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
import org.keycloak.testframework.realm.ManagedRealm;

@KeycloakIntegrationTest(config = SpiffeClientAuthTest.SpiffeServerConfig.class)
@TestMethodOrder(MethodOrderer.MethodName.class)
public class SpiffeClientAuthWithJwkUseSigTest extends AbstractClientAuthTest {

@InjectRealm(config = SpiffeClientAuthTest.ExernalClientAuthRealmConfig.class)
protected ManagedRealm realm;

@InjectOAuthIdentityProvider(config = SpiffeWithOidcIdpConfig.class)
OAuthIdentityProvider identityProvider;

public SpiffeClientAuthWithJwkUseSigTest() {
super(null, SpiffeClientAuthTest.INTERNAL_CLIENT_ID, SpiffeClientAuthTest.EXTERNAL_CLIENT_ID);
}

@Test
public void testWithIssClaimAndSigUseOnJwk() {
JsonWebToken jwt = createDefaultToken();
assertSuccess(SpiffeClientAuthTest.INTERNAL_CLIENT_ID, doClientGrant(createDefaultToken()));
assertSuccess(SpiffeClientAuthTest.INTERNAL_CLIENT_ID, jwt.getId(), "https://myissuer", SpiffeClientAuthTest.EXTERNAL_CLIENT_ID, events.poll());
}

@Override
protected String getClientAssertionType() {
return SpiffeConstants.CLIENT_ASSERTION_TYPE;
}

@Override
protected OAuthIdentityProvider getIdentityProvider() {
return identityProvider;
}

@Override
protected JsonWebToken createDefaultToken() {
JsonWebToken token = new JsonWebToken();
token.id(null);
token.issuer("https://myissuer");
token.audience(oAuthClient.getEndpoints().getIssuer());
token.exp((long) (Time.currentTime() + 300));
token.subject(SpiffeClientAuthTest.EXTERNAL_CLIENT_ID);
return token;
}

public static class SpiffeWithOidcIdpConfig implements OAuthIdentityProviderConfig {

@Override
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
return config.jwkUse(true);
}
}

}
Loading
Loading