mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
Experimental SPIFFE identity provider (#42314)
Closes #42313 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
fc467f48c8
commit
320ea5a9a7
@ -93,6 +93,8 @@ public class Profile {
|
||||
|
||||
CLIENT_AUTH_FEDERATED("Authenticates client based on assertions issued by identity provider", Type.EXPERIMENTAL),
|
||||
|
||||
SPIFFE("SPIFFE trust relationship provider", Type.EXPERIMENTAL),
|
||||
|
||||
// Check if kerberos is available in underlying JVM and auto-detect if feature should be enabled or disabled by default based on that
|
||||
KERBEROS("Kerberos", Type.DEFAULT, 1, () -> KerberosJdkProvider.getProvider().isKerberosAvailable()),
|
||||
|
||||
|
||||
@ -19,7 +19,8 @@ package org.keycloak.crypto;
|
||||
public enum KeyUse {
|
||||
|
||||
SIG("sig"),
|
||||
ENC("enc");
|
||||
ENC("enc"),
|
||||
JWT_SVID("jwt-svid");
|
||||
|
||||
private String specName;
|
||||
|
||||
|
||||
@ -16,26 +16,14 @@
|
||||
*/
|
||||
package org.keycloak.crypto;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.Key;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
public class KeyWrapper {
|
||||
|
||||
/**
|
||||
* A repository for the default algorithms by key type.
|
||||
*/
|
||||
private static final Map<String, String> DEFAULT_ALGORITHM_BY_TYPE = new HashMap<>();
|
||||
|
||||
static {
|
||||
//backwards compatibility: RSA keys without "alg" field set are considered RS256
|
||||
DEFAULT_ALGORITHM_BY_TYPE.put(KeyType.RSA, Algorithm.RS256);
|
||||
}
|
||||
|
||||
private String providerId;
|
||||
private long providerPriority;
|
||||
private String kid;
|
||||
@ -85,18 +73,34 @@ public class KeyWrapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the value of the optional {@code alg} claim. If not defined, a default is returned depending on the
|
||||
* key type as per {@code kty} claim.
|
||||
* <p>Returns the value of the optional {@code alg} claim. If not defined, a default is
|
||||
* inferred for some algorithms.
|
||||
*
|
||||
* <p>For keys of type {@link KeyType#RSA}, the default algorithm is {@link Algorithm#RS256} as this is the default
|
||||
* algorithm recommended by OIDC specs.
|
||||
*
|
||||
* <p>For keys of type {@link KeyType#EC}, {@link Algorithm#ES256}, {@link Algorithm#ES384}, or {@link Algorithm#ES512}
|
||||
* is returned based on the curve
|
||||
*
|
||||
* @return the algorithm set or a default based on the key type.
|
||||
*/
|
||||
public String getAlgorithmOrDefault() {
|
||||
if (algorithm == null) {
|
||||
return DEFAULT_ALGORITHM_BY_TYPE.get(type);
|
||||
if (algorithm == null && type != null) {
|
||||
switch (type) {
|
||||
case KeyType.EC:
|
||||
if (curve != null) {
|
||||
switch (curve) {
|
||||
case "P-256":
|
||||
return Algorithm.ES256;
|
||||
case "P-384":
|
||||
return Algorithm.ES384;
|
||||
case "P-512":
|
||||
return Algorithm.ES512;
|
||||
}
|
||||
}
|
||||
case KeyType.RSA:
|
||||
return Algorithm.RS256;
|
||||
}
|
||||
}
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
@ -17,11 +17,13 @@
|
||||
|
||||
package org.keycloak.jose.jwk;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class JSONWebKeySet {
|
||||
|
||||
@JsonProperty("keys")
|
||||
|
||||
@ -45,9 +45,14 @@ public class JWK {
|
||||
|
||||
public static final String SHA256_509_THUMBPRINT = "x5t#S256";
|
||||
|
||||
/**
|
||||
* This duplicates {@link org.keycloak.crypto.KeyUse}, which should be used instead when possible
|
||||
*/
|
||||
@Deprecated
|
||||
public enum Use {
|
||||
SIG("sig"),
|
||||
ENCRYPTION("enc");
|
||||
ENCRYPTION("enc"),
|
||||
JWT_SVID("jwt-svid");
|
||||
|
||||
private String str;
|
||||
|
||||
|
||||
@ -168,7 +168,7 @@ public abstract class JWKSUtilsTest {
|
||||
|
||||
key = keyWrappersForUse.getKeyByKidAndAlg(kidEC2, null);
|
||||
assertNotNull(key);
|
||||
assertNull(key.getAlgorithmOrDefault());
|
||||
assertEquals("ES384", key.getAlgorithmOrDefault());
|
||||
assertEquals(KeyUse.SIG, key.getUse());
|
||||
assertEquals(kidEC2, key.getKid());
|
||||
assertEquals("EC", key.getType());
|
||||
|
||||
@ -2,14 +2,16 @@ package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
public interface ClientAssertionContext {
|
||||
|
||||
RealmModel getRealm();
|
||||
ClientModel getClient();
|
||||
String getAssertionType();
|
||||
JWSInput getJwsInput();
|
||||
JsonWebToken getToken();
|
||||
ClientModel getClient();
|
||||
boolean isFailure();
|
||||
String getError();
|
||||
|
||||
|
||||
@ -882,7 +882,15 @@ public class RepresentationToModel {
|
||||
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
|
||||
updateOrganizationBroker(representation, session);
|
||||
identityProviderModel.setOrganizationId(representation.getOrganizationId());
|
||||
identityProviderModel.setConfig(removeEmptyString(representation.getConfig()));
|
||||
|
||||
// Merge config from the identity provider model in case the provider sets some default config
|
||||
Map<String, String> repConfig = removeEmptyString(representation.getConfig());
|
||||
if (repConfig != null && !repConfig.isEmpty()) {
|
||||
if (identityProviderModel.getConfig() == null) {
|
||||
identityProviderModel.setConfig(new HashMap<>());
|
||||
}
|
||||
identityProviderModel.getConfig().putAll(repConfig);
|
||||
}
|
||||
|
||||
String flowAlias = representation.getFirstBrokerLoginFlowAlias();
|
||||
if (flowAlias == null || flowAlias.trim().isEmpty()) {
|
||||
|
||||
@ -10,11 +10,13 @@ import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
|
||||
import org.keycloak.broker.provider.ClientAssertionContext;
|
||||
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.broker.spiffe.SpiffeConstants;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
@ -37,7 +39,7 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
new ProviderConfigProperty(JWT_CREDENTIAL_ISSUER_KEY, "Identity provider", "Issuer of the client assertion", ProviderConfigProperty.STRING_TYPE, null)
|
||||
);
|
||||
|
||||
private static final Set<String> SUPPORTED_ASSERTION_TYPES = Set.of(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
|
||||
private static final Set<String> SUPPORTED_ASSERTION_TYPES = Set.of(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT, SpiffeConstants.CLIENT_ASSERTION_TYPE);
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
@ -69,7 +71,7 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
|
||||
ClientAssertionIdentityProvider identityProvider = lookupIdentityProvider(context, client);
|
||||
|
||||
ClientAssertionContext clientAssertionContext = new DefaultClientAssertionContext(client, clientAssertionType, jws, token);
|
||||
ClientAssertionContext clientAssertionContext = new DefaultClientAssertionContext(context.getRealm(), client, clientAssertionType, jws, token);
|
||||
if (identityProvider.verifyClientAssertion(clientAssertionContext)) {
|
||||
context.success();
|
||||
} else {
|
||||
@ -155,19 +157,31 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
|
||||
private static class DefaultClientAssertionContext implements ClientAssertionContext {
|
||||
|
||||
private final RealmModel realm;
|
||||
private final ClientModel client;
|
||||
private final String assertionType;
|
||||
private final JWSInput jwsInput;
|
||||
private final JsonWebToken token;
|
||||
private String error;
|
||||
|
||||
public DefaultClientAssertionContext(ClientModel client, String assertionType, JWSInput jwsInput, JsonWebToken token) {
|
||||
public DefaultClientAssertionContext(RealmModel realm, ClientModel client, String assertionType, JWSInput jwsInput, JsonWebToken token) {
|
||||
this.realm = realm;
|
||||
this.client = client;
|
||||
this.assertionType = assertionType;
|
||||
this.jwsInput = jwsInput;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssertionType() {
|
||||
return assertionType;
|
||||
@ -183,11 +197,6 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFailure() {
|
||||
return error != null;
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
package org.keycloak.broker.spiffe;
|
||||
|
||||
import org.keycloak.crypto.PublicKeysWrapper;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.keys.PublicKeyLoader;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
|
||||
public class SpiffeBundleEndpointLoader implements PublicKeyLoader {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final String bundleEndpoint;
|
||||
|
||||
public SpiffeBundleEndpointLoader(KeycloakSession session, String bundleEndpoint) {
|
||||
this.session = session;
|
||||
this.bundleEndpoint = bundleEndpoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKeysWrapper loadKeys() throws Exception {
|
||||
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, bundleEndpoint);
|
||||
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.JWT_SVID);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package org.keycloak.broker.spiffe;
|
||||
|
||||
public interface SpiffeConstants {
|
||||
|
||||
String CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe";
|
||||
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
package org.keycloak.broker.spiffe;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.ClientAssertionContext;
|
||||
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Implementation for https://datatracker.ietf.org/doc/draft-schwenkschuster-oauth-spiffe-client-auth/
|
||||
*
|
||||
* Main differences for SPIFFE JWT SVIDs and regular client assertions:
|
||||
* <ul>
|
||||
* <li><code>jwt-spiffe</code> client assertion type</li>
|
||||
* <li><code>iss</code> claim is optional, uses SPIFFE IDs, which includes trust domain instead</li>
|
||||
* <li><code>jti</code> claim is optional, and SPIFFE vendors re-use/cache tokens</li>
|
||||
* <li><code>sub</code> is a SPIFFE ID with the syntax <code>spiffe://trust-domain/workload-identity</code></li>
|
||||
* <li>Keys are fetched from a SPIFFE bundle endpoint, where the JWKS has additional SPIFFE specific fields (<code>spiffe_sequence</code> and <code>spiffe_refresh_hint</code>, the JWK does not set the <code>alg></code></li>
|
||||
* </ul>
|
||||
*/
|
||||
public class SpiffeIdentityProvider implements IdentityProvider<SpiffeIdentityProviderConfig>, ClientAssertionIdentityProvider {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(SpiffeIdentityProvider.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final SpiffeIdentityProviderConfig config;
|
||||
|
||||
public SpiffeIdentityProvider(KeycloakSession session, SpiffeIdentityProviderConfig config) {
|
||||
this.session = session;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiffeIdentityProviderConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyClientAssertion(ClientAssertionContext context) {
|
||||
if (!context.getAssertionType().equals(SpiffeConstants.CLIENT_ASSERTION_TYPE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trustedDomain = config.getTrustDomain();
|
||||
|
||||
if (!verifySignature(context)) {
|
||||
return context.failure("Invalid signature");
|
||||
}
|
||||
|
||||
JsonWebToken token = context.getToken();
|
||||
|
||||
URI uri = URI.create(token.getSubject());
|
||||
if (!uri.getScheme().equals("spiffe")) {
|
||||
return context.failure("Not a SPIFFE ID");
|
||||
}
|
||||
|
||||
if (!uri.getRawAuthority().equals(trustedDomain)) {
|
||||
return context.failure("Invalid trust-domain");
|
||||
}
|
||||
|
||||
String expectedAudience = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), session.getContext().getRealm().getName());
|
||||
int allowedClockSkew = config.getAllowedClockSkew();
|
||||
|
||||
if (token.getExp() == null || token.getExp() <= 0) {
|
||||
return context.failure("Token does not contain an expiration");
|
||||
}
|
||||
|
||||
if (!(token.getAudience().length == 1 && token.getAudience()[0].equals(expectedAudience))) {
|
||||
return context.failure("Invalid audience");
|
||||
}
|
||||
|
||||
if (!token.isActive(allowedClockSkew)) {
|
||||
return context.failure("Token not active");
|
||||
}
|
||||
if (token.getIat() != null && token.getIat() > 0 && token.getIat() - allowedClockSkew > Time.currentTime()) {
|
||||
return context.failure("Token was issued in the future");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean verifySignature(ClientAssertionContext context) {
|
||||
|
||||
try {
|
||||
String bundleEndpoint = config.getBundleEndpoint();
|
||||
JWSInput jws = context.getJwsInput();
|
||||
JWSHeader header = jws.getHeader();
|
||||
String kid = header.getKeyId();
|
||||
String alg = header.getRawAlgorithm();
|
||||
|
||||
String modelKey = PublicKeyStorageUtils.getIdpModelCacheKey(context.getRealm().getId(), config.getInternalId());
|
||||
|
||||
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
|
||||
KeyWrapper publicKey = keyStorage.getPublicKey(modelKey, kid, alg, new SpiffeBundleEndpointLoader(session, bundleEndpoint));
|
||||
|
||||
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, alg);
|
||||
if (signatureProvider == null) {
|
||||
LOGGER.debugf("Failed to verify token, signature provider not found for algorithm %s", alg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return signatureProvider.verifier(publicKey).verify(jws.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8), jws.getSignature());
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Failed to verify token signature", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response performLogin(AuthenticationRequest request) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response export(UriInfo uriInfo, RealmModel realm, String format) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderDataMarshaller getMarshaller() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package org.keycloak.broker.spiffe;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IdentityProviderShowInAccountConsole;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.keycloak.common.util.UriUtils.checkUrl;
|
||||
|
||||
public class SpiffeIdentityProviderConfig extends IdentityProviderModel {
|
||||
|
||||
public static final String TRUST_DOMAIN_KEY = "trustDomain";
|
||||
public static final String BUNDLE_ENDPOINT_KEY = "bundleEndpoint";
|
||||
|
||||
private static final Pattern TRUST_DOMAIN_PATTERN = Pattern.compile("[a-z0-9.\\-_]*");
|
||||
|
||||
public SpiffeIdentityProviderConfig() {
|
||||
getConfig().put(IdentityProviderModel.SHOW_IN_ACCOUNT_CONSOLE, IdentityProviderShowInAccountConsole.NEVER.name());
|
||||
}
|
||||
|
||||
public SpiffeIdentityProviderConfig(IdentityProviderModel model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHideOnLogin() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getAllowedClockSkew() {
|
||||
String allowedClockSkew = getConfig().get(ALLOWED_CLOCK_SKEW);
|
||||
if (allowedClockSkew == null || allowedClockSkew.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(getConfig().get(ALLOWED_CLOCK_SKEW));
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore it and use default
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getTrustDomain() {
|
||||
return getConfig().get(TRUST_DOMAIN_KEY);
|
||||
}
|
||||
|
||||
public String getBundleEndpoint() {
|
||||
return getConfig().get(BUNDLE_ENDPOINT_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(RealmModel realm) {
|
||||
super.validate(realm);
|
||||
|
||||
String trustDomain = getTrustDomain();
|
||||
if (trustDomain == null || !TRUST_DOMAIN_PATTERN.matcher(trustDomain).matches()) {
|
||||
throw new IllegalArgumentException("Invalid trust domain name");
|
||||
}
|
||||
|
||||
checkUrl(realm.getSslRequired(), getBundleEndpoint(), BUNDLE_ENDPOINT_KEY);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package org.keycloak.broker.spiffe;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class SpiffeIdentityProviderFactory extends AbstractIdentityProviderFactory<SpiffeIdentityProvider> implements EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "spiffe";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "SPIFFE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiffeIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new SpiffeIdentityProvider(session, new SpiffeIdentityProviderConfig(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(KeycloakSession session, String configString) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel createConfig() {
|
||||
return new SpiffeIdentityProviderConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.SPIFFE);
|
||||
}
|
||||
|
||||
}
|
||||
@ -18,4 +18,5 @@
|
||||
org.keycloak.broker.oidc.OIDCIdentityProviderFactory
|
||||
org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory
|
||||
org.keycloak.broker.saml.SAMLIdentityProviderFactory
|
||||
org.keycloak.broker.oauth.OAuth2IdentityProviderFactory
|
||||
org.keycloak.broker.oauth.OAuth2IdentityProviderFactory
|
||||
org.keycloak.broker.spiffe.SpiffeIdentityProviderFactory
|
||||
@ -0,0 +1,8 @@
|
||||
package org.keycloak.testframework.oauth;
|
||||
|
||||
public class DefaultOAuthIdentityProviderConfig implements OAuthIdentityProviderConfig {
|
||||
@Override
|
||||
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,6 @@ import org.keycloak.crypto.ECDSASignatureSignerContext;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.def.DefaultCryptoProvider;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
@ -22,14 +21,18 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class OAuthIdentityProvider {
|
||||
|
||||
private final HttpServer httpServer;
|
||||
|
||||
private final OAuthIdentityProviderKeys keys;
|
||||
private final OAuthIdentityProviderConfigBuilder.OAuthIdentityProviderConfiguration config;
|
||||
|
||||
public OAuthIdentityProvider(HttpServer httpServer) {
|
||||
public OAuthIdentityProvider(HttpServer httpServer, OAuthIdentityProviderConfigBuilder.OAuthIdentityProviderConfiguration config) {
|
||||
this.config = config;
|
||||
if (!CryptoIntegration.isInitialised()) {
|
||||
CryptoIntegration.setProvider(new DefaultCryptoProvider());
|
||||
}
|
||||
@ -37,7 +40,7 @@ public class OAuthIdentityProvider {
|
||||
this.httpServer = httpServer;
|
||||
httpServer.createContext("/idp/jwks", new JwksHttpHandler());
|
||||
|
||||
keys = new OAuthIdentityProviderKeys();
|
||||
keys = new OAuthIdentityProviderKeys(config);
|
||||
}
|
||||
|
||||
public String encodeToken(JsonWebToken token) {
|
||||
@ -49,7 +52,7 @@ public class OAuthIdentityProvider {
|
||||
}
|
||||
|
||||
public OAuthIdentityProviderKeys createKeys() {
|
||||
return new OAuthIdentityProviderKeys();
|
||||
return new OAuthIdentityProviderKeys(config);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@ -75,19 +78,29 @@ public class OAuthIdentityProvider {
|
||||
|
||||
private final String jwksString;
|
||||
|
||||
public OAuthIdentityProviderKeys() {
|
||||
public OAuthIdentityProviderKeys(OAuthIdentityProviderConfigBuilder.OAuthIdentityProviderConfiguration config) {
|
||||
try {
|
||||
KeyUse keyUse = config.spiffe() ? KeyUse.JWT_SVID : KeyUse.SIG;
|
||||
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
|
||||
keyPairGenerator.initialize(ecSpec);
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
JWK jwk = JWKBuilder.create().ec(keyPair.getPublic());
|
||||
jwk.setAlgorithm("ES256");
|
||||
jwk.setPublicKeyUse(KeyUse.SIG.getSpecName());
|
||||
if (!config.spiffe()) {
|
||||
jwk.setAlgorithm("ES256");
|
||||
}
|
||||
jwk.setPublicKeyUse(keyUse.getSpecName());
|
||||
|
||||
Map<String, Object> jwks = new HashMap<>();
|
||||
jwks.put("keys", new JWK[] { jwk });
|
||||
|
||||
if (config.spiffe()) {
|
||||
jwks.put("spiffe_sequence", 1);
|
||||
jwks.put("spiffe_refresh_hint", 300);
|
||||
}
|
||||
|
||||
JSONWebKeySet jwks = new JSONWebKeySet();
|
||||
jwks.setKeys(new JWK[] { jwk });
|
||||
jwksString = JsonSerialization.writeValueAsString(jwks);
|
||||
|
||||
keyWrapper = new KeyWrapper();
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
package org.keycloak.testframework.oauth;
|
||||
|
||||
public interface OAuthIdentityProviderConfig {
|
||||
|
||||
OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config);
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package org.keycloak.testframework.oauth;
|
||||
|
||||
public class OAuthIdentityProviderConfigBuilder {
|
||||
|
||||
private boolean spiffe;
|
||||
|
||||
public OAuthIdentityProviderConfigBuilder spiffe() {
|
||||
spiffe = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuthIdentityProviderConfiguration build() {
|
||||
return new OAuthIdentityProviderConfiguration(spiffe);
|
||||
}
|
||||
|
||||
public record OAuthIdentityProviderConfiguration(boolean spiffe) {
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,6 +4,7 @@ import com.sun.net.httpserver.HttpServer;
|
||||
import org.keycloak.testframework.injection.InstanceContext;
|
||||
import org.keycloak.testframework.injection.RequestedInstance;
|
||||
import org.keycloak.testframework.injection.Supplier;
|
||||
import org.keycloak.testframework.injection.SupplierHelpers;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
|
||||
public class OAuthIdentityProviderSupplier implements Supplier<OAuthIdentityProvider, InjectOAuthIdentityProvider> {
|
||||
@ -11,12 +12,21 @@ public class OAuthIdentityProviderSupplier implements Supplier<OAuthIdentityProv
|
||||
@Override
|
||||
public OAuthIdentityProvider getValue(InstanceContext<OAuthIdentityProvider, InjectOAuthIdentityProvider> instanceContext) {
|
||||
HttpServer httpServer = instanceContext.getDependency(HttpServer.class);
|
||||
return new OAuthIdentityProvider(httpServer);
|
||||
OAuthIdentityProviderConfig config = SupplierHelpers.getInstance(instanceContext.getAnnotation().config());
|
||||
OAuthIdentityProviderConfigBuilder configBuilder = new OAuthIdentityProviderConfigBuilder();
|
||||
OAuthIdentityProviderConfigBuilder.OAuthIdentityProviderConfiguration configuration = config.configure(configBuilder).build();
|
||||
|
||||
return new OAuthIdentityProvider(httpServer, configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(InstanceContext<OAuthIdentityProvider, InjectOAuthIdentityProvider> instanceContext) {
|
||||
instanceContext.getValue().close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean compatible(InstanceContext<OAuthIdentityProvider, InjectOAuthIdentityProvider> a, RequestedInstance<OAuthIdentityProvider, InjectOAuthIdentityProvider> b) {
|
||||
return true;
|
||||
return a.getAnnotation().equals(b.getAnnotation());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package org.keycloak.testframework.oauth.annotations;
|
||||
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.oauth.DefaultOAuthIdentityProviderConfig;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -13,4 +15,6 @@ public @interface InjectOAuthIdentityProvider {
|
||||
|
||||
LifeCycle lifecycle() default LifeCycle.GLOBAL;
|
||||
|
||||
Class<? extends OAuthIdentityProviderConfig> config() default DefaultOAuthIdentityProviderConfig.class;
|
||||
|
||||
}
|
||||
|
||||
@ -23,6 +23,8 @@ import java.util.List;
|
||||
@KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class)
|
||||
public class FederatedClientAuthFromKeycloakTest {
|
||||
|
||||
private static final String IDP_ALIAS = "keycloak-idp";
|
||||
|
||||
@InjectRealm(config = InternalRealmConfig.class)
|
||||
ManagedRealm internalRealm;
|
||||
|
||||
@ -50,7 +52,7 @@ public class FederatedClientAuthFromKeycloakTest {
|
||||
return realm.identityProvider(
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias("external")
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute("issuer", "http://localhost:8080/realms/external")
|
||||
.setAttribute(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.USE_JWKS_URL, "true")
|
||||
@ -67,7 +69,7 @@ public class FederatedClientAuthFromKeycloakTest {
|
||||
return client.clientId("myclient")
|
||||
.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, "external");
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ import java.util.UUID;
|
||||
@KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class)
|
||||
public class FederatedClientAuthTest {
|
||||
|
||||
private static final String IDP_ALIAS = "myidp";
|
||||
private static final String IDP_ALIAS = "external-idp";
|
||||
|
||||
private static final String CLIENT_ID = "myclient";
|
||||
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
207
tests/base/src/test/java/org/keycloak/tests/client/authentication/external/SpiffeClientAuthTest.java
vendored
Normal file
207
tests/base/src/test/java/org/keycloak/tests/client/authentication/external/SpiffeClientAuthTest.java
vendored
Normal file
@ -0,0 +1,207 @@
|
||||
package org.keycloak.tests.client.authentication.external;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
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;
|
||||
import org.keycloak.broker.spiffe.SpiffeIdentityProviderFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Time;
|
||||
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 {
|
||||
|
||||
private static final String IDP_ALIAS = "spiffe-idp";
|
||||
|
||||
private static final String CLIENT_ID = "spiffe://mytrust-domain/myclient";
|
||||
|
||||
@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()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidConfig() {
|
||||
testInvalidConfig("with-port:8080", "https://localhost");
|
||||
testInvalidConfig("spiffe://with-spiffe-scheme", "https://localhost");
|
||||
testInvalidConfig("valid", "invalid-url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidTrustDomain() {
|
||||
IdentityProviderUpdater.updateWithRollback(realm, IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(SpiffeIdentityProviderConfig.TRUST_DOMAIN_KEY, "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));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReuse() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
}
|
||||
|
||||
private boolean doClientGrant(JsonWebToken token) {
|
||||
String jws = identityProvider.encodeToken(token);
|
||||
return doClientGrant(jws);
|
||||
}
|
||||
|
||||
private boolean doClientGrant(String jws) {
|
||||
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, SpiffeConstants.CLIENT_ASSERTION_TYPE).send();
|
||||
return response.isSuccess();
|
||||
}
|
||||
|
||||
private JsonWebToken createDefaultToken() {
|
||||
JsonWebToken token = new JsonWebToken();
|
||||
token.id(null);
|
||||
token.audience(oAuthClient.getEndpoints().getIssuer());
|
||||
token.exp((long) (Time.currentTime() + 300));
|
||||
token.subject(CLIENT_ID);
|
||||
return token;
|
||||
}
|
||||
|
||||
private void testInvalidConfig(String trustDomain, String bundleEndpoint) {
|
||||
IdentityProviderRepresentation idp = IdentityProviderBuilder.create().providerId(SpiffeIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias("another")
|
||||
.setAttribute(SpiffeIdentityProviderConfig.TRUST_DOMAIN_KEY, trustDomain)
|
||||
.setAttribute(SpiffeIdentityProviderConfig.BUNDLE_ENDPOINT_KEY, bundleEndpoint).build();
|
||||
|
||||
try (Response r = realm.admin().identityProviders().create(idp)) {
|
||||
Assertions.assertEquals(400, r.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
public static class SpiffeServerConfig extends ClientAuthIdpServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return super.configure(config).features(Profile.Feature.SPIFFE);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SpiffeIdpConfig implements OAuthIdentityProviderConfig {
|
||||
|
||||
@Override
|
||||
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
|
||||
return config.spiffe();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExernalClientAuthRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
return realm.identityProvider(
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(SpiffeIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute(SpiffeIdentityProviderConfig.TRUST_DOMAIN_KEY, "mytrust-domain")
|
||||
.setAttribute(SpiffeIdentityProviderConfig.BUNDLE_ENDPOINT_KEY, "http://127.0.0.1:8500/idp/jwks")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExernalClientAuthClientConfig implements ClientConfig {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder client) {
|
||||
return client.clientId(CLIENT_ID)
|
||||
.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user