Skip to content

Commit 84fd00c

Browse files
authored
SPIFFE should support OIDC JWK endpoint (#43651) (#43656)
Closes #43650 (cherry picked from commit f6ac649) Signed-off-by: stianst <[email protected]>
1 parent 4ad4ce5 commit 84fd00c

File tree

11 files changed

+264
-102
lines changed

11 files changed

+264
-102
lines changed

docs/documentation/server_admin/topics/identity-broker/spiffe.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ image:images/spiffe-add-identity-provider.png[Add SPIFFE Provider]
2828
|The SPIFFE Trust domain (for example `spiffe://my-trust-domain`)
2929

3030
|SPIFFE Bundle Endpoint
31-
|`https` URL for the SPIFFE Bundle Endpoint where the SPIFFE servers public keys are exposed
31+
|`https` URL for the SPIFFE Bundle Endpoint or the OpenID Connect JWKS endpoint where the SPIFFE servers public keys are exposed
3232
|===

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ addSamlProvider=Add SAML provider
940940
addSpiffeProvider=Add SPIFFE provider
941941
addKubernetesProvider=Add Kubernetes provider
942942
spiffeTrustDomain=SPIFFE Trust Domain
943-
spiffeBundleEndpoint=SPIFFE Bundle Endpoint
943+
spiffeBundleEndpoint=SPIFFE Bundle or OIDC JWKs endpoint
944944
kubernetesJWKSURL=Kubernetes JWKS URL
945945
kubernetesJWKSURLHelp=Use Kubernetes JWKS URL when accessing an external Kubernetes cluster. The JWKS endpoint must not require authentication
946946
permission=Permission

services/src/main/java/org/keycloak/broker/spiffe/SpiffeBundleEndpointLoader.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ public SpiffeBundleEndpointLoader(KeycloakSession session, String bundleEndpoint
2121
@Override
2222
public PublicKeysWrapper loadKeys() throws Exception {
2323
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, bundleEndpoint);
24-
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.JWT_SVID);
24+
PublicKeysWrapper keysWrapper = JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.JWT_SVID, true);
25+
if (keysWrapper.getKeys().isEmpty()) {
26+
keysWrapper = JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG, true);
27+
}
28+
return keysWrapper;
2529
}
2630

2731
}

test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProvider.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ public OAuthIdentityProviderKeys(OAuthIdentityProviderConfigBuilder.OAuthIdentit
9191
if (!config.spiffe()) {
9292
jwk.setAlgorithm("ES256");
9393
}
94-
jwk.setPublicKeyUse(keyUse.getSpecName());
94+
if (config.jwkUse()) {
95+
jwk.setPublicKeyUse(keyUse.getSpecName());
96+
} else {
97+
jwk.setPublicKeyUse(null);
98+
}
9599

96100
Map<String, Object> jwks = new HashMap<>();
97101
jwks.put("keys", new JWK[] { jwk });

test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProviderConfigBuilder.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@
33
public class OAuthIdentityProviderConfigBuilder {
44

55
private boolean spiffe;
6+
private boolean jwkUse = true;
67

78
public OAuthIdentityProviderConfigBuilder spiffe() {
89
spiffe = true;
910
return this;
1011
}
1112

13+
public OAuthIdentityProviderConfigBuilder jwkUse(boolean jwkUse) {
14+
this.jwkUse = jwkUse;
15+
return this;
16+
}
17+
1218
public OAuthIdentityProviderConfiguration build() {
13-
return new OAuthIdentityProviderConfiguration(spiffe);
19+
return new OAuthIdentityProviderConfiguration(spiffe, jwkUse);
1420
}
1521

16-
public record OAuthIdentityProviderConfiguration(boolean spiffe) {
22+
public record OAuthIdentityProviderConfiguration(boolean spiffe, boolean jwkUse) {
1723
}
1824

1925
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.keycloak.tests.client.authentication.external;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
import org.keycloak.common.util.Time;
6+
import org.keycloak.representations.JsonWebToken;
7+
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
8+
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
9+
10+
public abstract class AbstractBaseClientAuthTest extends AbstractClientAuthTest {
11+
12+
public AbstractBaseClientAuthTest(String expectedTokenIssuer, String internalClientId, String externalClientId) {
13+
super(expectedTokenIssuer, internalClientId, externalClientId);
14+
}
15+
16+
@Test
17+
public void testValidToken() {
18+
JsonWebToken token = createDefaultToken();
19+
assertSuccess(internalClientId, doClientGrant(token));
20+
assertSuccess(internalClientId, token.getId(), expectedTokenIssuer, externalClientId, events.poll());
21+
}
22+
23+
@Test
24+
public void testInvalidSignature() {
25+
OAuthIdentityProvider.OAuthIdentityProviderKeys keys = getIdentityProvider().createKeys();
26+
JsonWebToken jwt = createDefaultToken();
27+
String jws = getIdentityProvider().encodeToken(jwt, keys);
28+
assertFailure("Invalid client or Invalid client credentials", doClientGrant(jws));
29+
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
30+
}
31+
32+
@Test
33+
public void testInvalidSub() {
34+
JsonWebToken jwt = createDefaultToken();
35+
jwt.subject("invalid");
36+
Assertions.assertFalse(doClientGrant(jwt).isSuccess());
37+
assertFailure(null, expectedTokenIssuer, "invalid", jwt.getId(), "client_not_found", events.poll());
38+
}
39+
40+
@Test
41+
public void testExpired() {
42+
JsonWebToken jwt = createDefaultToken();
43+
jwt.exp((long) (Time.currentTime() - 30));
44+
assertFailure("Token is not active", doClientGrant(jwt));
45+
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
46+
}
47+
48+
@Test
49+
public void testMissingExp() {
50+
JsonWebToken jwt = createDefaultToken();
51+
jwt.exp(null);
52+
assertFailure("Token exp claim is required", doClientGrant(jwt));
53+
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
54+
}
55+
56+
@Test
57+
public void testInvalidNbf() {
58+
JsonWebToken jwt = createDefaultToken();
59+
jwt.nbf((long) (Time.currentTime() + 60));
60+
assertFailure("Token is not active", doClientGrant(jwt));
61+
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
62+
}
63+
64+
@Test
65+
public void testInvalidAud() {
66+
JsonWebToken jwt = createDefaultToken();
67+
jwt.audience("invalid");
68+
assertFailure("Invalid token audience", doClientGrant(jwt));
69+
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
70+
}
71+
72+
@Test
73+
public void testMissingAud() {
74+
JsonWebToken jwt = createDefaultToken();
75+
jwt.audience((String) null);
76+
assertFailure("Invalid token audience", doClientGrant(jwt));
77+
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
78+
}
79+
80+
@Test
81+
public void testMultipleAud() {
82+
JsonWebToken jwt = createDefaultToken();
83+
jwt.audience(jwt.getAudience()[0], "invalid");
84+
assertFailure("Multiple audiences not allowed", doClientGrant(jwt));
85+
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
86+
}
87+
88+
@Test
89+
public void testValidInvalidAssertionType() {
90+
JsonWebToken jwt = createDefaultToken();
91+
String jws = getIdentityProvider().encodeToken(jwt);
92+
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, "urn:ietf:params:oauth:client-assertion-type:invalid").send();
93+
assertFailure(response);
94+
assertFailure(null, expectedTokenIssuer, externalClientId, jwt.getId(), "client_not_found", events.poll());
95+
}
96+
97+
}

tests/base/src/test/java/org/keycloak/tests/client/authentication/external/AbstractFederatedClientAuthTest.java renamed to tests/base/src/test/java/org/keycloak/tests/client/authentication/external/AbstractClientAuthTest.java

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package org.keycloak.tests.client.authentication.external;
22

33
import org.junit.jupiter.api.Assertions;
4-
import org.junit.jupiter.api.Test;
54
import org.keycloak.OAuth2Constants;
6-
import org.keycloak.common.util.Time;
75
import org.keycloak.events.EventType;
86
import org.keycloak.representations.AccessToken;
97
import org.keycloak.representations.JsonWebToken;
@@ -16,105 +14,24 @@
1614
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
1715
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
1816

19-
public abstract class AbstractFederatedClientAuthTest {
17+
public abstract class AbstractClientAuthTest {
2018

21-
private final String expectedTokenIssuer;
22-
private final String internalClientId;
23-
private final String externalClientId;
19+
final String expectedTokenIssuer;
20+
final String internalClientId;
21+
final String externalClientId;
2422

2523
@InjectOAuthClient
2624
OAuthClient oAuthClient;
2725

2826
@InjectEvents
2927
Events events;
3028

31-
public AbstractFederatedClientAuthTest(String expectedTokenIssuer, String internalClientId, String externalClientId) {
29+
public AbstractClientAuthTest(String expectedTokenIssuer, String internalClientId, String externalClientId) {
3230
this.expectedTokenIssuer = expectedTokenIssuer;
3331
this.internalClientId = internalClientId;
3432
this.externalClientId = externalClientId;
3533
}
3634

37-
@Test
38-
public void testValidToken() {
39-
JsonWebToken token = createDefaultToken();
40-
assertSuccess(internalClientId, doClientGrant(token));
41-
assertSuccess(internalClientId, token.getId(), expectedTokenIssuer, externalClientId, events.poll());
42-
}
43-
44-
@Test
45-
public void testInvalidSignature() {
46-
OAuthIdentityProvider.OAuthIdentityProviderKeys keys = getIdentityProvider().createKeys();
47-
JsonWebToken jwt = createDefaultToken();
48-
String jws = getIdentityProvider().encodeToken(jwt, keys);
49-
assertFailure("Invalid client or Invalid client credentials", doClientGrant(jws));
50-
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
51-
}
52-
53-
@Test
54-
public void testInvalidSub() {
55-
JsonWebToken jwt = createDefaultToken();
56-
jwt.subject("invalid");
57-
Assertions.assertFalse(doClientGrant(jwt).isSuccess());
58-
assertFailure(null, expectedTokenIssuer, "invalid", jwt.getId(), "client_not_found", events.poll());
59-
}
60-
61-
@Test
62-
public void testExpired() {
63-
JsonWebToken jwt = createDefaultToken();
64-
jwt.exp((long) (Time.currentTime() - 30));
65-
assertFailure("Token is not active", doClientGrant(jwt));
66-
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
67-
}
68-
69-
@Test
70-
public void testMissingExp() {
71-
JsonWebToken jwt = createDefaultToken();
72-
jwt.exp(null);
73-
assertFailure("Token exp claim is required", doClientGrant(jwt));
74-
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
75-
}
76-
77-
@Test
78-
public void testInvalidNbf() {
79-
JsonWebToken jwt = createDefaultToken();
80-
jwt.nbf((long) (Time.currentTime() + 60));
81-
assertFailure("Token is not active", doClientGrant(jwt));
82-
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
83-
}
84-
85-
@Test
86-
public void testInvalidAud() {
87-
JsonWebToken jwt = createDefaultToken();
88-
jwt.audience("invalid");
89-
assertFailure("Invalid token audience", doClientGrant(jwt));
90-
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
91-
}
92-
93-
@Test
94-
public void testMissingAud() {
95-
JsonWebToken jwt = createDefaultToken();
96-
jwt.audience((String) null);
97-
assertFailure("Invalid token audience", doClientGrant(jwt));
98-
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
99-
}
100-
101-
@Test
102-
public void testMultipleAud() {
103-
JsonWebToken jwt = createDefaultToken();
104-
jwt.audience(jwt.getAudience()[0], "invalid");
105-
assertFailure("Multiple audiences not allowed", doClientGrant(jwt));
106-
assertFailure(internalClientId, expectedTokenIssuer, externalClientId, jwt.getId(), events.poll());
107-
}
108-
109-
@Test
110-
public void testValidInvalidAssertionType() {
111-
JsonWebToken jwt = createDefaultToken();
112-
String jws = getIdentityProvider().encodeToken(jwt);
113-
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, "urn:ietf:params:oauth:client-assertion-type:invalid").send();
114-
assertFailure(response);
115-
assertFailure(null, expectedTokenIssuer, externalClientId, jwt.getId(), "client_not_found", events.poll());
116-
}
117-
11835
protected abstract OAuthIdentityProvider getIdentityProvider();
11936

12037
protected abstract JsonWebToken createDefaultToken();

tests/base/src/test/java/org/keycloak/tests/client/authentication/external/FederatedClientAuthTest.java renamed to tests/base/src/test/java/org/keycloak/tests/client/authentication/external/BaseClientAuthTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.UUID;
2020

2121
@KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class)
22-
public class FederatedClientAuthTest extends AbstractFederatedClientAuthTest {
22+
public class BaseClientAuthTest extends AbstractBaseClientAuthTest {
2323

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

@@ -33,7 +33,7 @@ public class FederatedClientAuthTest extends AbstractFederatedClientAuthTest {
3333
@InjectOAuthIdentityProvider
3434
OAuthIdentityProvider identityProvider;
3535

36-
public FederatedClientAuthTest() {
36+
public BaseClientAuthTest() {
3737
super(TOKEN_ISSUER, INTERNAL_CLIENT_ID, EXTERNAL_CLIENT_ID);
3838
}
3939

tests/base/src/test/java/org/keycloak/tests/client/authentication/external/SpiffeClientAuthTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@
2525

2626
@KeycloakIntegrationTest(config = SpiffeClientAuthTest.SpiffeServerConfig.class)
2727
@TestMethodOrder(MethodOrderer.MethodName.class)
28-
public class SpiffeClientAuthTest extends AbstractFederatedClientAuthTest {
28+
public class SpiffeClientAuthTest extends AbstractBaseClientAuthTest {
2929

30-
private static final String INTERNAL_CLIENT_ID = "myclient";
31-
private static final String EXTERNAL_CLIENT_ID = "spiffe://mytrust-domain/myclient";
32-
private static final String IDP_ALIAS = "spiffe-idp";
33-
private static final String TRUST_DOMAIN = "spiffe://mytrust-domain";
34-
private static final String BUNDLE_ENDPOINT = "http://127.0.0.1:8500/idp/jwks";
30+
static final String INTERNAL_CLIENT_ID = "myclient";
31+
static final String EXTERNAL_CLIENT_ID = "spiffe://mytrust-domain/myclient";
32+
static final String IDP_ALIAS = "spiffe-idp";
33+
static final String TRUST_DOMAIN = "spiffe://mytrust-domain";
34+
static final String BUNDLE_ENDPOINT = "http://127.0.0.1:8500/idp/jwks";
3535

3636
@InjectRealm(config = ExernalClientAuthRealmConfig.class)
3737
protected ManagedRealm realm;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.keycloak.tests.client.authentication.external;
2+
3+
import org.junit.jupiter.api.MethodOrderer;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.TestMethodOrder;
6+
import org.keycloak.broker.spiffe.SpiffeConstants;
7+
import org.keycloak.common.util.Time;
8+
import org.keycloak.representations.JsonWebToken;
9+
import org.keycloak.testframework.annotations.InjectRealm;
10+
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
11+
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
12+
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
13+
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder;
14+
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
15+
import org.keycloak.testframework.realm.ManagedRealm;
16+
17+
@KeycloakIntegrationTest(config = SpiffeClientAuthTest.SpiffeServerConfig.class)
18+
@TestMethodOrder(MethodOrderer.MethodName.class)
19+
public class SpiffeClientAuthWithJwkUseSigTest extends AbstractClientAuthTest {
20+
21+
@InjectRealm(config = SpiffeClientAuthTest.ExernalClientAuthRealmConfig.class)
22+
protected ManagedRealm realm;
23+
24+
@InjectOAuthIdentityProvider(config = SpiffeWithOidcIdpConfig.class)
25+
OAuthIdentityProvider identityProvider;
26+
27+
public SpiffeClientAuthWithJwkUseSigTest() {
28+
super(null, SpiffeClientAuthTest.INTERNAL_CLIENT_ID, SpiffeClientAuthTest.EXTERNAL_CLIENT_ID);
29+
}
30+
31+
@Test
32+
public void testWithIssClaimAndSigUseOnJwk() {
33+
JsonWebToken jwt = createDefaultToken();
34+
assertSuccess(SpiffeClientAuthTest.INTERNAL_CLIENT_ID, doClientGrant(createDefaultToken()));
35+
assertSuccess(SpiffeClientAuthTest.INTERNAL_CLIENT_ID, jwt.getId(), "https://myissuer", SpiffeClientAuthTest.EXTERNAL_CLIENT_ID, events.poll());
36+
}
37+
38+
@Override
39+
protected String getClientAssertionType() {
40+
return SpiffeConstants.CLIENT_ASSERTION_TYPE;
41+
}
42+
43+
@Override
44+
protected OAuthIdentityProvider getIdentityProvider() {
45+
return identityProvider;
46+
}
47+
48+
@Override
49+
protected JsonWebToken createDefaultToken() {
50+
JsonWebToken token = new JsonWebToken();
51+
token.id(null);
52+
token.issuer("https://myissuer");
53+
token.audience(oAuthClient.getEndpoints().getIssuer());
54+
token.exp((long) (Time.currentTime() + 300));
55+
token.subject(SpiffeClientAuthTest.EXTERNAL_CLIENT_ID);
56+
return token;
57+
}
58+
59+
public static class SpiffeWithOidcIdpConfig implements OAuthIdentityProviderConfig {
60+
61+
@Override
62+
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
63+
return config.jwkUse(true);
64+
}
65+
}
66+
67+
}

0 commit comments

Comments
 (0)