mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-21 06:20:05 -06:00
Experimental Kube service accounts identity provider
Closes #37600 Signed-off-by: stianst <stianst@gmail.com> Signed-off-by: Ryan Emerson <remerson@ibm.com> Co-authored-by: Ryan Emerson <remerson@ibm.com>
This commit is contained in:
@@ -95,6 +95,8 @@ public class Profile {
|
||||
|
||||
SPIFFE("SPIFFE trust relationship provider", Type.PREVIEW),
|
||||
|
||||
KUBERNETES_SERVICE_ACCOUNTS("Kubernetes service accounts 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()),
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.keycloak.broker.kubernetes;
|
||||
|
||||
public interface KubernetesConstants {
|
||||
|
||||
String KUBERNETES_SERVICE_HOST_KEY = "KUBERNETES_SERVICE_HOST";
|
||||
String KUBERNETES_SERVICE_PORT_HTTPS_KEY = "KUBERNETES_SERVICE_PORT_HTTPS";
|
||||
String SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.keycloak.broker.kubernetes;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
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.sessions.AuthenticationSessionModel;
|
||||
|
||||
public class KubernetesIdentityProvider extends OIDCIdentityProvider {
|
||||
|
||||
private final String globalJwksUrl;
|
||||
|
||||
public KubernetesIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config, String globalJwksUrl) {
|
||||
super(session, config);
|
||||
this.globalJwksUrl = globalJwksUrl;
|
||||
}
|
||||
|
||||
protected KeyWrapper getIdentityProviderKeyWrapper(JWSInput jws) {
|
||||
JWSHeader header = jws.getHeader();
|
||||
String kid = header.getKeyId();
|
||||
String alg = header.getRawAlgorithm();
|
||||
|
||||
String modelKey = PublicKeyStorageUtils.getIdpModelCacheKey(session.getContext().getRealm().getId(), getConfig().getInternalId());
|
||||
|
||||
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
|
||||
return keyStorage.getPublicKey(modelKey, kid, alg, new KubernetesJwksEndpointLoader(session, globalJwksUrl, getConfig().getJwksUrl()));
|
||||
}
|
||||
|
||||
@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,61 @@
|
||||
package org.keycloak.broker.kubernetes;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
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;
|
||||
|
||||
import static org.keycloak.broker.kubernetes.KubernetesConstants.KUBERNETES_SERVICE_HOST_KEY;
|
||||
import static org.keycloak.broker.kubernetes.KubernetesConstants.KUBERNETES_SERVICE_PORT_HTTPS_KEY;
|
||||
|
||||
public class KubernetesIdentityProviderFactory extends AbstractIdentityProviderFactory<KubernetesIdentityProvider> implements EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "kubernetes";
|
||||
|
||||
private String globalJwksUrl;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Kubernetes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public KubernetesIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new KubernetesIdentityProvider(session, new OIDCIdentityProviderConfig(model), globalJwksUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
String kubernetesServiceHost = System.getenv(KUBERNETES_SERVICE_HOST_KEY);
|
||||
String kubernetesServicePortHttps = System.getenv(KUBERNETES_SERVICE_PORT_HTTPS_KEY);
|
||||
if (kubernetesServiceHost != null && kubernetesServicePortHttps != null) {
|
||||
globalJwksUrl = "https://" + kubernetesServiceHost + ":" + kubernetesServicePortHttps + "/openid/v1/jwks";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(KeycloakSession session, String configString) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel createConfig() {
|
||||
return new OIDCIdentityProviderConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.KUBERNETES_SERVICE_ACCOUNTS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.keycloak.broker.kubernetes;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
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.util.JWKSUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.keycloak.broker.kubernetes.KubernetesConstants.SERVICE_ACCOUNT_TOKEN_PATH;
|
||||
|
||||
public class KubernetesJwksEndpointLoader implements PublicKeyLoader {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
private final boolean authenticate;
|
||||
private final String endpoint;
|
||||
|
||||
public KubernetesJwksEndpointLoader(KeycloakSession session, String globalEndpoint, String providerEndpoint) {
|
||||
this.session = session;
|
||||
|
||||
if (globalEndpoint == null && providerEndpoint == null) {
|
||||
throw new RuntimeException("Not running on Kubernetes and Kubernetes JWKS endpoint not set");
|
||||
}
|
||||
|
||||
if (providerEndpoint == null || providerEndpoint.isEmpty() || globalEndpoint.equals(providerEndpoint)) {
|
||||
this.endpoint = globalEndpoint;
|
||||
authenticate = true;
|
||||
} else {
|
||||
this.endpoint = providerEndpoint;
|
||||
authenticate = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKeysWrapper loadKeys() throws Exception {
|
||||
CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
|
||||
HttpGet httpGet = new HttpGet(endpoint);
|
||||
|
||||
httpGet.setHeader(HttpHeaders.ACCEPT, "application/jwk-set+json");
|
||||
|
||||
if (authenticate) {
|
||||
String token = FileUtils.readFileToString(new File(SERVICE_ACCOUNT_TOKEN_PATH), StandardCharsets.UTF_8);
|
||||
httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
}
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
|
||||
JSONWebKeySet jwks = JsonSerialization.readValue(response.getEntity().getContent(), JSONWebKeySet.class);
|
||||
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,5 @@ org.keycloak.broker.oidc.OIDCIdentityProviderFactory
|
||||
org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory
|
||||
org.keycloak.broker.saml.SAMLIdentityProviderFactory
|
||||
org.keycloak.broker.oauth.OAuth2IdentityProviderFactory
|
||||
org.keycloak.broker.spiffe.SpiffeIdentityProviderFactory
|
||||
org.keycloak.broker.spiffe.SpiffeIdentityProviderFactory
|
||||
org.keycloak.broker.kubernetes.KubernetesIdentityProviderFactory
|
||||
Reference in New Issue
Block a user