/*
* 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 javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.keys.KeyProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.ApiUtil;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCKeycloakServerBrokerWithSignatureTest extends AbstractIdentityProviderTest {
private static final int PORT = 8082;
private static Keycloak keycloak1;
private static Keycloak keycloak2;
@ClassRule
public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() {
@Override
protected void configureServer(KeycloakServer server) {
server.getConfig().setPort(PORT);
}
@Override
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json"));
}
@Override
protected String[] getTestRealms() {
return new String[] { "realm-with-oidc-identity-provider" };
}
};
@BeforeClass
public static void beforeClazz() {
keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
}
@Override
public void onBefore() {
super.onBefore();
// Enable validate signatures
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setValidateSignature(true);
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
}
@Override
protected String getProviderId() {
return "kc-oidc-idp";
}
@Test
public void testSignatureVerificationJwksUrl() throws Exception {
// Configure OIDC identity provider with JWKS URL
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setUseJwksUrl(true);
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT));
String jwksUrl = b.build("realm-with-oidc-identity-provider").toString();
cfg.setJwksUrl(jwksUrl);
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
// Check that user is able to login
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
// Rotate public keys on the parent broker
rotateKeys("realm-with-oidc-identity-provider");
RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation();
realm.setPublicKey(org.keycloak.models.Constants.GENERATE);
keycloak2.realm("realm-with-oidc-identity-provider").update(realm);
// User not able to login now as new keys can't be yet downloaded (10s timeout)
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
// Set time offset. New keys can be downloaded. Check that user is able to login.
Time.setOffset(20);
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
Time.setOffset(0);
}
@Test
public void testSignatureVerificationHardcodedPublicKey() throws Exception {
// Configure OIDC identity provider with publicKeySignatureVerifier
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setUseJwksUrl(false);
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(keycloak2.realm("realm-with-oidc-identity-provider"));
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
// Check that user is able to login
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
// Rotate public keys on the parent broker
rotateKeys("realm-with-oidc-identity-provider");
// User not able to login now as new keys can't be yet downloaded (10s timeout)
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
// Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
Time.setOffset(20);
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
Time.setOffset(0);
}
private void rotateKeys(String realmName) {
String activeKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA");
// Rotate public keys on the parent broker
String realmId = keycloak2.realm(realmName).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 = keycloak2.realm("realm-with-oidc-identity-provider").components().add(keys);
assertEquals(201, response.getStatus());
response.close();
String updatedActiveKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA");
assertNotEquals(activeKid, updatedActiveKid);
}
}