Thanks to visit codestin.com
Credit goes to github.com

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 @@ -11,6 +11,7 @@ public class FederatedJWTClientValidator extends AbstractJWTClientValidator {
private final String expectedTokenIssuer;
private final int allowedClockSkew;
private final boolean reusePermitted;
private int maximumExpirationTime = 300;

public FederatedJWTClientValidator(ClientAuthenticationFlowContext context, SignatureValidator signatureValidator, String expectedTokenIssuer, int allowedClockSkew, boolean reusePermitted) throws Exception {
super(context, signatureValidator, null);
Expand Down Expand Up @@ -41,7 +42,11 @@ protected int getAllowedClockSkew() {

@Override
protected int getMaximumExpirationTime() {
return 300; // TODO Hard-coded for now, but should be configurable
return maximumExpirationTime;
}

public void setMaximumExpirationTime(int maximumExpirationTime) {
this.maximumExpirationTime = maximumExpirationTime;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,96 +1,72 @@
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.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
import org.jboss.logging.Logger;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.authentication.authenticators.client.AbstractJWTClientValidator;
import org.keycloak.authentication.authenticators.client.FederatedJWTClientValidator;
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.events.EventBuilder;
import org.keycloak.crypto.SignatureProvider;
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 {
import java.nio.charset.StandardCharsets;

public class KubernetesIdentityProvider implements ClientAssertionIdentityProvider<KubernetesIdentityProviderConfig> {

private static final Logger LOGGER = Logger.getLogger(KubernetesIdentityProvider.class);

private final KeycloakSession session;
private final KubernetesIdentityProviderConfig config;
private final String globalJwksUrl;

public KubernetesIdentityProvider(KeycloakSession session, KubernetesIdentityProviderConfig config, String globalJwksUrl) {
super(session, config);
this.session = session;
this.config = 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();
public boolean verifyClientAssertion(ClientAuthenticationFlowContext context) throws Exception {
FederatedJWTClientValidator validator = new FederatedJWTClientValidator(context, this::verifySignature, config.getIssuer(), config.getAllowedClockSkew(), true);
validator.setMaximumExpirationTime(3600); // Kubernetes defaults to 1 hour (https://kubernetes.io/docs/concepts/storage/projected-volumes/#serviceaccounttoken)
return validator.validate();
}

@Override
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
throw new UnsupportedOperationException();
private boolean verifySignature(AbstractJWTClientValidator validator) {
try {
JWSInput jws = validator.getState().getJws();
JWSHeader header = jws.getHeader();
String kid = header.getKeyId();
String alg = header.getRawAlgorithm();

String modelKey = PublicKeyStorageUtils.getIdpModelCacheKey(validator.getContext().getRealm().getId(), config.getInternalId());
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
KeyWrapper publicKey = keyStorage.getPublicKey(modelKey, kid, alg, new KubernetesJwksEndpointLoader(session, globalJwksUrl, getConfig().getJwksUrl()));

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 updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
throw new UnsupportedOperationException();
public KubernetesIdentityProviderConfig getConfig() {
return config;
}

@Override
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
throw new UnsupportedOperationException();
}

@Override
public Response performLogin(AuthenticationRequest request) {
throw new UnsupportedOperationException();
}
public void close() {

@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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,39 @@

import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderShowInAccountConsole;

public class KubernetesIdentityProviderConfig extends OIDCIdentityProviderConfig {
public class KubernetesIdentityProviderConfig extends IdentityProviderModel {

public static final String ISSUER = OIDCIdentityProviderConfig.ISSUER;

public static final String JWKS_URL = OIDCIdentityProviderConfig.JWKS_URL;

public KubernetesIdentityProviderConfig() {
this(null);
}

public KubernetesIdentityProviderConfig(IdentityProviderModel model) {
super(model);
setHideOnLogin(true);
getConfig().put(IdentityProviderModel.SHOW_IN_ACCOUNT_CONSOLE, IdentityProviderShowInAccountConsole.NEVER.name());
}

public String getIssuer() {
return getConfig().get(ISSUER);
}

public String getJwksUrl() {
return getConfig().get(JWKS_URL);
}

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;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public KubernetesJwksEndpointLoader(KeycloakSession session, String globalEndpoi
throw new RuntimeException("Not running on Kubernetes and Kubernetes JWKS endpoint not set");
}

if (providerEndpoint == null || providerEndpoint.isEmpty() || globalEndpoint.equals(providerEndpoint)) {
if (globalEndpoint != null && !globalEndpoint.isEmpty() && globalEndpoint.equals(providerEndpoint)) {
this.endpoint = globalEndpoint;
authenticate = true;
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.keycloak.tests.client.authentication.external;

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.authentication.authenticators.client.FederatedJWTClientAuthenticator;
import org.keycloak.broker.kubernetes.KubernetesIdentityProviderConfig;
import org.keycloak.broker.kubernetes.KubernetesIdentityProviderFactory;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.models.IdentityProviderModel;
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.annotations.InjectOAuthIdentityProvider;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.RealmConfigBuilder;
import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testsuite.util.IdentityProviderBuilder;

import java.util.UUID;

@KeycloakIntegrationTest(config = KubernetesClientAuthTest.KubernetesServerConfig.class)
@TestMethodOrder(MethodOrderer.MethodName.class)
public class KubernetesClientAuthTest extends AbstractBaseClientAuthTest {

static final String INTERNAL_CLIENT_ID = "myclient";
static final String EXTERNAL_CLIENT_ID = "system:serviceaccount:mynamespace:myserviceaccount";
static final String IDP_ALIAS = "kubernetes-idp";
static final String ISSUER = "https://kubernetes.default.svc.cluster.local";

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

@InjectOAuthIdentityProvider
OAuthIdentityProvider identityProvider;

@InjectTimeOffSet
TimeOffSet timeOffSet;

public KubernetesClientAuthTest() {
super(ISSUER, INTERNAL_CLIENT_ID, EXTERNAL_CLIENT_ID);
}

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

@Test
public void testKeysCached() {
Assertions.assertTrue(doClientGrant(createDefaultToken()).isSuccess());
int keysRequestCount = identityProvider.getKeysRequestCount();
Assertions.assertTrue(doClientGrant(createDefaultToken()).isSuccess());
Assertions.assertEquals(keysRequestCount, identityProvider.getKeysRequestCount());
}

@Test
public void testInvalidIssuer() {
JsonWebToken jwt = createDefaultToken();
jwt.issuer("https://invalid");
assertFailure(doClientGrant(jwt));
assertFailure(null, "https://invalid", jwt.getSubject(), jwt.getId(), "client_not_found", events.poll());
}

@Test
public void testOldIAt() {
JsonWebToken jwt = createDefaultToken();
jwt.iat((long) (Time.currentTime() - 3550));
assertSuccess(internalClientId, doClientGrant(jwt));
assertSuccess(internalClientId, jwt.getId(), expectedTokenIssuer, externalClientId, events.poll());
}

@Test
public void testReuse() {
JsonWebToken jwt = createDefaultToken();
assertSuccess(internalClientId, doClientGrant(jwt));
assertSuccess(internalClientId, jwt.getId(), expectedTokenIssuer, externalClientId, events.poll());
assertSuccess(internalClientId, doClientGrant(jwt));
assertSuccess(internalClientId, jwt.getId(), expectedTokenIssuer, externalClientId, events.poll());
}

@Override
protected JsonWebToken createDefaultToken() {
JsonWebToken token = new JsonWebToken();
token.id(UUID.randomUUID().toString());
token.issuer(ISSUER);
token.audience(oAuthClient.getEndpoints().getIssuer());
token.nbf((long) (Time.currentTime()));
token.exp((long) (Time.currentTime() + 300));
token.iat((long) (Time.currentTime() - 300));
token.subject(EXTERNAL_CLIENT_ID);
return token;
}

public static class KubernetesServerConfig extends ClientAuthIdpServerConfig {

@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return super.configure(config).features(Profile.Feature.KUBERNETES_SERVICE_ACCOUNTS);
}
}

public static class ExernalClientAuthRealmConfig implements RealmConfig {

@Override
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
realm.identityProvider(
IdentityProviderBuilder.create()
.providerId(KubernetesIdentityProviderFactory.PROVIDER_ID)
.alias(IDP_ALIAS)
.setAttribute(IdentityProviderModel.ISSUER, ISSUER)
.setAttribute(KubernetesIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
.build());

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;
}
}

}
Loading