/*
* 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.testsuite.broker;
import java.util.List;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.*;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.PublicKeyStorageUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestingCacheResource;
import org.keycloak.testsuite.util.OAuthClient;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcOidcBrokerConfiguration.INSTANCE;
}
@Before
public void createUser() {
log.debug("creating user for realm " + bc.providerRealmName());
UserRepresentation user = new UserRepresentation();
user.setUsername(bc.getUserLogin());
user.setEmail(bc.getUserEmail());
user.setEmailVerified(true);
user.setEnabled(true);
RealmResource realmResource = adminClient.realm(bc.providerRealmName());
String userId = createUserWithAdminClient(realmResource, user);
resetUserPassword(realmResource.users().get(userId), bc.getUserPassword(), false);
}
// TODO: Possibly move to parent superclass
@Before
public void addIdentityProviderToProviderRealm() {
log.debug("adding identity provider to realm " + bc.consumerRealmName());
RealmResource realm = adminClient.realm(bc.consumerRealmName());
Response resp = realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext));
resp.close();
}
@Before
public void addClients() {
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
if (clients != null) {
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName());
Response resp = providerRealm.clients().create(client);
resp.close();
}
}
clients = bc.createConsumerClients(suiteContext);
if (clients != null) {
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName());
Response resp = consumerRealm.clients().create(client);
resp.close();
}
}
}
@Test
public void testSignatureVerificationJwksUrl() throws Exception {
// Configure OIDC identity provider with JWKS URL
updateIdentityProviderWithJwksUrl();
// Check that user is able to login
logInAsUserInIDPForFirstTime();
assertLoggedInAccountManagement();
logoutFromRealm(bc.consumerRealmName());
// Rotate public keys on the parent broker
rotateKeys();
// User not able to login now as new keys can't be yet downloaded (10s timeout)
logInAsUserInIDP();
assertErrorPage("Unexpected error when authenticating with identity provider");
logoutFromRealm(bc.consumerRealmName());
// Set time offset. New keys can be downloaded. Check that user is able to login.
setTimeOffset(20);
logInAsUserInIDP();
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 {
// Configure OIDC identity provider with JWKS URL
IdentityProviderRepresentation idpRep = getIdentityProvider();
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
cfg.setValidateSignature(true);
cfg.setUseJwksUrl(false);
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(providerRealm());
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
updateIdentityProvider(idpRep);
// Check that user is able to login
logInAsUserInIDPForFirstTime();
assertLoggedInAccountManagement();
logoutFromRealm(bc.consumerRealmName());
// Rotate public keys on the parent broker
rotateKeys();
// User not able to login now as new keys can't be yet downloaded (10s timeout)
logInAsUserInIDP();
assertErrorPage("Unexpected error when authenticating with identity provider");
logoutFromRealm(bc.consumerRealmName());
// Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
setTimeOffset(20);
logInAsUserInIDP();
assertErrorPage("Unexpected error when authenticating with identity provider");
}
@Test
public void testSignatureVerificationHardcodedPublicKeyWithKeyIdSetExplicitly() throws Exception {
// Configure OIDC identity provider with JWKS URL
IdentityProviderRepresentation idpRep = getIdentityProvider();
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
cfg.setValidateSignature(true);
cfg.setUseJwksUrl(false);
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(providerRealm());
String pemData = key.getPublicKey();
cfg.setPublicKeySignatureVerifier(pemData);
String expectedKeyId = KeyUtils.createKeyId(PemUtils.decodePublicKey(pemData));
updateIdentityProvider(idpRep);
// Check that user is able to login
logInAsUserInIDPForFirstTime();
assertLoggedInAccountManagement();
logoutFromRealm(bc.consumerRealmName());
// Set key id to an invalid one
cfg.setPublicKeySignatureVerifierKeyId("invalid-key-id");
updateIdentityProvider(idpRep);
logInAsUserInIDP();
assertErrorPage("Unexpected error when authenticating with identity provider");
// Set key id to a valid one
cfg.setPublicKeySignatureVerifierKeyId(expectedKeyId);
updateIdentityProvider(idpRep);
logInAsUserInIDP();
assertLoggedInAccountManagement();
logoutFromRealm(bc.consumerRealmName());
// Set key id to empty
cfg.setPublicKeySignatureVerifierKeyId("");
updateIdentityProvider(idpRep);
logInAsUserInIDP();
assertLoggedInAccountManagement();
logoutFromRealm(bc.consumerRealmName());
// Unset key id
cfg.setPublicKeySignatureVerifierKeyId(null);
updateIdentityProvider(idpRep);
logInAsUserInIDP();
assertLoggedInAccountManagement();
logoutFromRealm(bc.consumerRealmName());
}
@Test
public void testClearKeysCache() 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));
// Clear cache and check nothing cached
consumerRealm().clearKeysCache();
Assert.assertFalse(cache.contains(expectedCacheKey));
Assert.assertEquals(cache.size(), 0);
}
// 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");
// Rotate public keys on the parent broker
String realmId = providerRealm().toRepresentation().getId();
ComponentRepresentation keys = new ComponentRepresentation();
keys.setName("generated");
keys.setProviderType(KeyProvider.class.getName());
keys.setProviderId("rsa-generated");
keys.setParentId(realmId);
keys.setConfig(new MultivaluedHashMap<>());
keys.getConfig().putSingle("priority", Long.toString(System.currentTimeMillis()));
Response response = providerRealm().components().add(keys);
assertEquals(201, response.getStatus());
response.close();
String updatedActiveKid = providerRealm().keys().getKeyMetadata().getActive().get("RSA");
assertNotEquals(activeKid, updatedActiveKid);
}
private RealmResource providerRealm() {
return adminClient.realm(bc.providerRealmName());
}
private IdentityProviderRepresentation getIdentityProvider() {
return consumerRealm().identityProviders().get(BrokerTestConstants.IDP_OIDC_ALIAS).toRepresentation();
}
private void updateIdentityProvider(IdentityProviderRepresentation rep) {
consumerRealm().identityProviders().get(BrokerTestConstants.IDP_OIDC_ALIAS).update(rep);
}
private RealmResource consumerRealm() {
return adminClient.realm(bc.consumerRealmName());
}
}