KEYCLOAK-3822 Changing signature validation settings of an external IdP is not sometimes reflected
This commit is contained in:
parent
cb4f856ab6
commit
69ce1e05f0
13 changed files with 491 additions and 70 deletions
|
@ -19,6 +19,7 @@ package org.keycloak.keys.infinispan;
|
|||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
@ -32,6 +33,7 @@ import org.keycloak.common.util.Time;
|
|||
import org.keycloak.keys.PublicKeyLoader;
|
||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.cache.infinispan.ClearCacheEvent;
|
||||
import org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory;
|
||||
|
||||
|
@ -51,13 +53,17 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
|
|||
|
||||
private final int minTimeBetweenRequests ;
|
||||
|
||||
private Set<String> invalidations = new HashSet<>();
|
||||
|
||||
public InfinispanPublicKeyStorageProvider(KeycloakSession session, Cache<String, PublicKeysEntry> keys, Map<String, FutureTask<PublicKeysEntry>> tasksInProgress, int minTimeBetweenRequests) {
|
||||
this.session = session;
|
||||
this.keys = keys;
|
||||
this.tasksInProgress = tasksInProgress;
|
||||
this.minTimeBetweenRequests = minTimeBetweenRequests;
|
||||
session.getTransactionManager().enlistAfterCompletion(getAfterTransaction());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
keys.clear();
|
||||
|
@ -65,6 +71,56 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
|
|||
cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
|
||||
}
|
||||
|
||||
|
||||
void addInvalidation(String cacheKey) {
|
||||
this.invalidations.add(cacheKey);
|
||||
}
|
||||
|
||||
|
||||
protected KeycloakTransaction getAfterTransaction() {
|
||||
return new KeycloakTransaction() {
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
runInvalidations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
runInvalidations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRollbackOnly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected void runInvalidations() {
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
|
||||
for (String cacheKey : invalidations) {
|
||||
keys.remove(cacheKey);
|
||||
cluster.notify(cacheKey, PublicKeyStorageInvalidationEvent.create(cacheKey), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public PublicKey getPublicKey(String modelKey, String kid, PublicKeyLoader loader) {
|
||||
// Check if key is in cache
|
||||
|
|
|
@ -30,9 +30,14 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
|||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageSpi;
|
||||
import org.keycloak.keys.PublicKeyStorageProviderFactory;
|
||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
import org.keycloak.provider.ProviderEventListener;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -45,7 +50,7 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
|||
|
||||
public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS";
|
||||
|
||||
private Cache<String, PublicKeysEntry> keysCache;
|
||||
private volatile Cache<String, PublicKeysEntry> keysCache;
|
||||
|
||||
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
|
||||
|
||||
|
@ -64,6 +69,15 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
|||
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
|
||||
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
|
||||
|
||||
if (event instanceof PublicKeyStorageInvalidationEvent) {
|
||||
PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event;
|
||||
keysCache.remove(invalidationEvent.getCacheKey());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
cluster.registerListener(KEYS_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
|
||||
|
||||
keysCache.clear();
|
||||
|
@ -82,6 +96,55 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
|||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
factory.register(new ProviderEventListener() {
|
||||
|
||||
@Override
|
||||
public void onEvent(ProviderEvent event) {
|
||||
if (keysCache == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SessionAndKeyHolder cacheKey = getCacheKeyToInvalidate(event);
|
||||
if (cacheKey != null) {
|
||||
log.debugf("Invalidating %s from keysCache", cacheKey);
|
||||
InfinispanPublicKeyStorageProvider provider = (InfinispanPublicKeyStorageProvider) cacheKey.session.getProvider(PublicKeyStorageProvider.class, getId());
|
||||
provider.addInvalidation(cacheKey.cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private SessionAndKeyHolder getCacheKeyToInvalidate(ProviderEvent event) {
|
||||
if (event instanceof RealmModel.ClientUpdatedEvent) {
|
||||
RealmModel.ClientUpdatedEvent eventt = (RealmModel.ClientUpdatedEvent) event;
|
||||
String cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getUpdatedClient().getRealm().getId(), eventt.getUpdatedClient().getId());
|
||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
||||
} else if (event instanceof RealmModel.ClientRemovedEvent) {
|
||||
RealmModel.ClientRemovedEvent eventt = (RealmModel.ClientRemovedEvent) event;
|
||||
String cacheKey = PublicKeyStorageUtils.getClientModelCacheKey(eventt.getClient().getRealm().getId(), eventt.getClient().getId());
|
||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
||||
} else if (event instanceof RealmModel.IdentityProviderUpdatedEvent) {
|
||||
RealmModel.IdentityProviderUpdatedEvent eventt = (RealmModel.IdentityProviderUpdatedEvent) event;
|
||||
String cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getUpdatedIdentityProvider().getInternalId());
|
||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
||||
} else if (event instanceof RealmModel.IdentityProviderRemovedEvent) {
|
||||
RealmModel.IdentityProviderRemovedEvent eventt = (RealmModel.IdentityProviderRemovedEvent) event;
|
||||
String cacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(eventt.getRealm().getId(), eventt.getRemovedIdentityProvider().getInternalId());
|
||||
return new SessionAndKeyHolder(eventt.getKeycloakSession(), cacheKey);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class SessionAndKeyHolder {
|
||||
private final KeycloakSession session;
|
||||
private final String cacheKey;
|
||||
|
||||
public SessionAndKeyHolder(KeycloakSession session, String cacheKey) {
|
||||
this.session = session;
|
||||
this.cacheKey = cacheKey;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys.infinispan;
|
||||
|
||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class PublicKeyStorageInvalidationEvent extends InvalidationEvent {
|
||||
|
||||
private String cacheKey;
|
||||
|
||||
public static PublicKeyStorageInvalidationEvent create(String cacheKey) {
|
||||
PublicKeyStorageInvalidationEvent event = new PublicKeyStorageInvalidationEvent();
|
||||
event.cacheKey = cacheKey;
|
||||
return event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
public String getCacheKey() {
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PublicKeyStorageInvalidationEvent [ " + cacheKey + " ]";
|
||||
}
|
||||
}
|
|
@ -960,6 +960,15 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
List<IdentityProviderModel> identityProviders = new ArrayList<IdentityProviderModel>();
|
||||
|
||||
for (IdentityProviderEntity entity: entities) {
|
||||
IdentityProviderModel identityProviderModel = entityToModel(entity);
|
||||
|
||||
identityProviders.add(identityProviderModel);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(identityProviders);
|
||||
}
|
||||
|
||||
private IdentityProviderModel entityToModel(IdentityProviderEntity entity) {
|
||||
IdentityProviderModel identityProviderModel = new IdentityProviderModel();
|
||||
identityProviderModel.setProviderId(entity.getProviderId());
|
||||
identityProviderModel.setAlias(entity.getAlias());
|
||||
|
@ -977,11 +986,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId());
|
||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||
|
||||
identityProviders.add(identityProviderModel);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(identityProviders);
|
||||
return identityProviderModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1024,8 +1029,28 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
public void removeIdentityProviderByAlias(String alias) {
|
||||
for (IdentityProviderEntity entity : realm.getIdentityProviders()) {
|
||||
if (entity.getAlias().equals(alias)) {
|
||||
|
||||
em.remove(entity);
|
||||
em.flush();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.IdentityProviderRemovedEvent() {
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return RealmAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel getRemovedIdentityProvider() {
|
||||
return entityToModel(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1048,6 +1073,24 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
}
|
||||
|
||||
em.flush();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.IdentityProviderUpdatedEvent() {
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return RealmAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel getUpdatedIdentityProvider() {
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -775,6 +775,15 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
List<IdentityProviderModel> identityProviders = new ArrayList<IdentityProviderModel>();
|
||||
|
||||
for (IdentityProviderEntity entity: entities) {
|
||||
IdentityProviderModel identityProviderModel = entityToModel(entity);
|
||||
|
||||
identityProviders.add(identityProviderModel);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(identityProviders);
|
||||
}
|
||||
|
||||
private IdentityProviderModel entityToModel(IdentityProviderEntity entity) {
|
||||
IdentityProviderModel identityProviderModel = new IdentityProviderModel();
|
||||
|
||||
identityProviderModel.setProviderId(entity.getProviderId());
|
||||
|
@ -792,11 +801,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
identityProviderModel.setPostBrokerLoginFlowId(entity.getPostBrokerLoginFlowId());
|
||||
identityProviderModel.setStoreToken(entity.isStoreToken());
|
||||
identityProviderModel.setAddReadTokenRoleOnCreate(entity.isAddReadTokenRoleOnCreate());
|
||||
|
||||
identityProviders.add(identityProviderModel);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(identityProviders);
|
||||
return identityProviderModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -837,6 +842,25 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
if (entity.getAlias().equals(alias)) {
|
||||
realm.getIdentityProviders().remove(entity);
|
||||
updateRealm();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.IdentityProviderRemovedEvent() {
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return RealmAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel getRemovedIdentityProvider() {
|
||||
return entityToModel(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -860,6 +884,24 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
}
|
||||
|
||||
updateRealm();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.IdentityProviderUpdatedEvent() {
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return RealmAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel getUpdatedIdentityProvider() {
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class PublicKeyStorageUtils {
|
||||
|
||||
public static String getClientModelCacheKey(String realmId, String clientUuid) {
|
||||
return realmId + "::client::" + clientUuid;
|
||||
}
|
||||
|
||||
public static String getIdpModelCacheKey(String realmId, String idpInternalId) {
|
||||
return realmId + "::idp::" + idpInternalId;
|
||||
}
|
||||
|
||||
}
|
|
@ -67,6 +67,18 @@ public interface RealmModel extends RoleContainerModel {
|
|||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
interface IdentityProviderUpdatedEvent extends ProviderEvent {
|
||||
RealmModel getRealm();
|
||||
IdentityProviderModel getUpdatedIdentityProvider();
|
||||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
interface IdentityProviderRemovedEvent extends ProviderEvent {
|
||||
RealmModel getRealm();
|
||||
IdentityProviderModel getRemovedIdentityProvider();
|
||||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
String getId();
|
||||
|
||||
String getName();
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.security.PublicKey;
|
|||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -36,27 +37,20 @@ public class PublicKeyStorageManager {
|
|||
|
||||
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
|
||||
|
||||
String modelKey = getModelKey(client);
|
||||
String modelKey = PublicKeyStorageUtils.getClientModelCacheKey(client.getRealm().getId(), client.getId());
|
||||
ClientPublicKeyLoader loader = new ClientPublicKeyLoader(session, client);
|
||||
return keyStorage.getPublicKey(modelKey, kid, loader);
|
||||
}
|
||||
|
||||
private static String getModelKey(ClientModel client) {
|
||||
return client.getRealm().getId() + "::client::" + client.getId();
|
||||
}
|
||||
|
||||
|
||||
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
|
||||
String kid = input.getHeader().getKeyId();
|
||||
|
||||
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
|
||||
|
||||
String modelKey = getModelKey(realm, idpConfig);
|
||||
String modelKey = PublicKeyStorageUtils.getIdpModelCacheKey(realm.getId(), idpConfig.getInternalId());
|
||||
OIDCIdentityProviderPublicKeyLoader loader = new OIDCIdentityProviderPublicKeyLoader(session, idpConfig);
|
||||
return keyStorage.getPublicKey(modelKey, kid, loader);
|
||||
}
|
||||
|
||||
private static String getModelKey(RealmModel realm, OIDCIdentityProviderConfig idpConfig) {
|
||||
return realm.getId() + "::idp::" + idpConfig.getInternalId();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ import javax.ws.rs.Path;
|
|||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
|
@ -63,13 +65,20 @@ public class TestingOIDCEndpointsApplicationResource {
|
|||
@NoCache
|
||||
public Map<String, String> generateKeys() {
|
||||
try {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(2048);
|
||||
clientData.setSigningKeyPair(generator.generateKeyPair());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
|
||||
clientData.setSigningKeyPair(keyPair);
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException("Error generating signing keypair", e);
|
||||
}
|
||||
|
||||
return getKeysAsPem();
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/get-keys-as-pem")
|
||||
public Map<String, String> getKeysAsPem() {
|
||||
String privateKeyPem = PemUtils.encodeKey(clientData.getSigningKeyPair().getPrivate());
|
||||
String publicKeyPem = PemUtils.encodeKey(clientData.getSigningKeyPair().getPublic());
|
||||
|
||||
|
|
|
@ -37,6 +37,11 @@ public interface TestOIDCEndpointsApplicationResource {
|
|||
@Path("/generate-keys")
|
||||
Map<String, String> generateKeys();
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/get-keys-as-pem")
|
||||
Map<String, String> getKeysAsPem();
|
||||
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -28,6 +28,8 @@ import org.keycloak.admin.client.resource.RealmResource;
|
|||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||
import org.keycloak.keys.loader.PublicKeyStorageManager;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
|
@ -107,15 +109,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
@Test
|
||||
public void testSignatureVerificationJwksUrl() throws Exception {
|
||||
// Configure OIDC identity provider with JWKS URL
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(true);
|
||||
|
||||
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
|
||||
String jwksUrl = b.build(bc.providerRealmName()).toString();
|
||||
cfg.setJwksUrl(jwksUrl);
|
||||
updateIdentityProvider(idpRep);
|
||||
updateIdentityProviderWithJwksUrl();
|
||||
|
||||
// Check that user is able to login
|
||||
logInAsUserInIDPForFirstTime();
|
||||
|
@ -139,6 +133,19 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
assertLoggedInAccountManagement();
|
||||
}
|
||||
|
||||
// Configure OIDC identity provider with JWKS URL and validateSignature=true
|
||||
private void updateIdentityProviderWithJwksUrl() {
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(true);
|
||||
|
||||
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
|
||||
String jwksUrl = b.build(bc.providerRealmName()).toString();
|
||||
cfg.setJwksUrl(jwksUrl);
|
||||
updateIdentityProvider(idpRep);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationHardcodedPublicKey() throws Exception {
|
||||
|
@ -178,23 +185,17 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
@Test
|
||||
public void testClearKeysCache() throws Exception {
|
||||
// Configure OIDC identity provider with JWKS URL
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(true);
|
||||
|
||||
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
|
||||
String jwksUrl = b.build(bc.providerRealmName()).toString();
|
||||
cfg.setJwksUrl(jwksUrl);
|
||||
updateIdentityProvider(idpRep);
|
||||
updateIdentityProviderWithJwksUrl();
|
||||
|
||||
// Check that user is able to login
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
|
||||
logoutFromRealm(bc.consumerRealmName());
|
||||
|
||||
// Check that key is cached
|
||||
String expectedCacheKey = consumerRealm().toRepresentation().getId() + "::idp::" + idpRep.getInternalId();
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
String expectedCacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(consumerRealm().toRepresentation().getId(), idpRep.getInternalId());
|
||||
TestingCacheResource cache = testingClient.testing(bc.consumerRealmName()).cache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
|
||||
Assert.assertTrue(cache.contains(expectedCacheKey));
|
||||
|
||||
|
@ -205,6 +206,40 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
|||
}
|
||||
|
||||
|
||||
// Test that when I update identityProvier, then the record in publicKey cache is cleared and it's not possible to authenticate with it anymore
|
||||
@Test
|
||||
public void testPublicKeyCacheInvalidatedWhenProviderUpdated() throws Exception {
|
||||
// Configure OIDC identity provider with JWKS URL
|
||||
updateIdentityProviderWithJwksUrl();
|
||||
|
||||
// Check that user is able to login
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
|
||||
logoutFromRealm(bc.consumerRealmName());
|
||||
|
||||
// Check that key is cached
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
String expectedCacheKey = PublicKeyStorageUtils.getIdpModelCacheKey(consumerRealm().toRepresentation().getId(), idpRep.getInternalId());
|
||||
TestingCacheResource cache = testingClient.testing(bc.consumerRealmName()).cache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
|
||||
Assert.assertTrue(cache.contains(expectedCacheKey));
|
||||
|
||||
// Update identityProvider to some bad JWKS_URL
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setJwksUrl("http://localhost:43214/non-existent");
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
// Check that key is not cached anymore
|
||||
Assert.assertFalse(cache.contains(expectedCacheKey));
|
||||
|
||||
// Check that user is not able to login with IDP
|
||||
setTimeOffset(20);
|
||||
logInAsUserInIDP();
|
||||
assertErrorPage("Unexpected error when authenticating with identity provider");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void rotateKeys() {
|
||||
String activeKid = providerRealm().keys().getKeyMetadata().getActive().get("RSA");
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
|
|||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation rep = new RealmRepresentation();
|
||||
rep.setEnabled(true);
|
||||
rep.setId(REALM_NAME);
|
||||
rep.setRealm(REALM_NAME);
|
||||
rep.setUsers(new LinkedList<UserRepresentation>());
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.testsuite.client;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
|
@ -40,9 +41,14 @@ import org.keycloak.OAuth2Constants;
|
|||
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
|
||||
import org.keycloak.client.registration.Auth;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.constants.ServiceUrlConstants;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||
import org.keycloak.keys.loader.PublicKeyStorageManager;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
|
@ -139,6 +145,16 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
|
|||
// The "kid" is set manually to some custom value
|
||||
@Test
|
||||
public void createClientWithJWKS_customKid() throws Exception {
|
||||
OIDCClientRepresentation response = createClientWithManuallySetKid("a1");
|
||||
|
||||
Map<String, String> generatedKeys = testingClient.testApp().oidcClientEndpoints().getKeysAsPem();
|
||||
|
||||
// Tries to authenticate client with privateKey JWT
|
||||
assertAuthenticateClientSuccess(generatedKeys, response, "a1");
|
||||
}
|
||||
|
||||
|
||||
private OIDCClientRepresentation createClientWithManuallySetKid(String kid) throws Exception {
|
||||
OIDCClientRepresentation clientRep = createRep();
|
||||
|
||||
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
|
||||
|
@ -146,20 +162,84 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
|
|||
|
||||
// Generate keys for client
|
||||
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
||||
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
|
||||
oidcClientEndpointsResource.generateKeys();
|
||||
|
||||
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
|
||||
|
||||
// Override kid with custom value
|
||||
keySet.getKeys()[0].setKeyId("a1");
|
||||
keySet.getKeys()[0].setKeyId(kid);
|
||||
clientRep.setJwks(keySet);
|
||||
|
||||
OIDCClientRepresentation response = reg.oidc().create(clientRep);
|
||||
return reg.oidc().create(clientRep);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testTwoClientsWithSameKid() throws Exception {
|
||||
// Create client with manually set "kid"
|
||||
OIDCClientRepresentation response = createClientWithManuallySetKid("a1");
|
||||
|
||||
|
||||
// Create client2
|
||||
OIDCClientRepresentation clientRep2 = createRep();
|
||||
|
||||
clientRep2.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
|
||||
clientRep2.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
|
||||
|
||||
// Generate some random keys for client2
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(2048);
|
||||
PublicKey client2PublicKey = generator.generateKeyPair().getPublic();
|
||||
|
||||
// Set client2 with manually set "kid" to be same like kid of client1 (but keys for both clients are different)
|
||||
JSONWebKeySet keySet = new JSONWebKeySet();
|
||||
keySet.setKeys(new JWK[]{JWKBuilder.create().kid("a1").rs256(client2PublicKey)});
|
||||
|
||||
clientRep2.setJwks(keySet);
|
||||
clientRep2 = reg.oidc().create(clientRep2);
|
||||
|
||||
|
||||
// Authenticate client1
|
||||
Map<String, String> generatedKeys = testingClient.testApp().oidcClientEndpoints().getKeysAsPem();
|
||||
assertAuthenticateClientSuccess(generatedKeys, response, "a1");
|
||||
|
||||
// Assert item in publicKey cache for client1
|
||||
String expectedCacheKey = PublicKeyStorageUtils.getClientModelCacheKey(REALM_NAME, response.getClientId());
|
||||
Assert.assertTrue(testingClient.testing().cache(InfinispanConnectionProvider.KEYS_CACHE_NAME).contains(expectedCacheKey));
|
||||
|
||||
// Assert it's not possible to authenticate as client2 with the same "kid" like client1
|
||||
assertAuthenticateClientError(generatedKeys, clientRep2, "a1");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testPublicKeyCacheInvalidatedWhenUpdatingClient() throws Exception {
|
||||
OIDCClientRepresentation response = createClientWithManuallySetKid("a1");
|
||||
|
||||
Map<String, String> generatedKeys = testingClient.testApp().oidcClientEndpoints().getKeysAsPem();
|
||||
|
||||
// Tries to authenticate client with privateKey JWT
|
||||
assertAuthenticateClientSuccess(generatedKeys, response, "a1");
|
||||
|
||||
// Assert item in publicKey cache for client1
|
||||
String expectedCacheKey = PublicKeyStorageUtils.getClientModelCacheKey(REALM_NAME, response.getClientId());
|
||||
Assert.assertTrue(testingClient.testing().cache(InfinispanConnectionProvider.KEYS_CACHE_NAME).contains(expectedCacheKey));
|
||||
|
||||
|
||||
|
||||
// Update client with some bad JWKS_URI
|
||||
response.setJwksUri("http://localhost:4321/non-existent");
|
||||
reg.auth(Auth.token(response.getRegistrationAccessToken()))
|
||||
.oidc().update(response);
|
||||
|
||||
// Assert item not any longer for client1
|
||||
Assert.assertFalse(testingClient.testing().cache(InfinispanConnectionProvider.KEYS_CACHE_NAME).contains(expectedCacheKey));
|
||||
|
||||
// Assert it's not possible to authenticate as client1
|
||||
assertAuthenticateClientError(generatedKeys, response, "a1");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void createClientWithJWKSURI() throws Exception {
|
||||
OIDCClientRepresentation clientRep = createRep();
|
||||
|
|
Loading…
Reference in a new issue