/* * 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.keys; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; import org.keycloak.RSATokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.PemUtils; import org.keycloak.keys.Attributes; import org.keycloak.keys.GeneratedHmacKeyProviderFactory; import org.keycloak.keys.KeyProvider; import org.keycloak.keys.ImportedRsaKeyProviderFactory; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.UserInfoClientUtil; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.core.Response; import java.io.IOException; import java.security.KeyPair; import java.security.PublicKey; import java.util.LinkedList; import java.util.List; import static org.junit.Assert.*; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; /** * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> */ public class KeyRotationTest extends AbstractKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); @Page protected AppPage appPage; @Page protected LoginPage loginPage; @Override public void addTestRealms(List<RealmRepresentation> testRealms) { RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); testRealms.add(realm); ClientRepresentation confApp = KeycloakModelUtils.createClient(realm, "confidential-cli"); confApp.setSecret("secret1"); confApp.setServiceAccountsEnabled(Boolean.TRUE); } @Test public void testIdentityCookie() throws Exception { // Create keys #1 createKeys1(); // Login with keys #1 loginPage.open(); loginPage.login("test-user@localhost", "password"); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); // Create keys #2 createKeys2(); // Login again with cookie signed with old keys appPage.open(); oauth.openLoginForm(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); // Drop key #1 dropKeys1(); // Login again with key #1 dropped - should pass as cookie should be refreshed appPage.open(); oauth.openLoginForm(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); // Drop key #2 dropKeys2(); // Login again with key #2 dropped - should fail as cookie hasn't been refreshed appPage.open(); oauth.openLoginForm(); assertTrue(loginPage.isCurrent()); } @Test public void testTokens() throws Exception { // Create keys #1 PublicKey key1 = createKeys1(); // Get token with keys #1 oauth.doLogin("test-user@localhost", "password"); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get("code"), "password"); assertEquals(200, response.getStatusCode()); assertTokenSignature(key1, response.getAccessToken()); assertTokenSignature(key1, response.getRefreshToken()); // Userinfo with keys #1 assertUserInfo(response.getAccessToken(), 200); // Token introspection with keys #1 assertTokenIntrospection(response.getAccessToken(), true); // Create keys #2 PublicKey key2 = createKeys2(); // Refresh token with keys #2 response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password"); assertEquals(200, response.getStatusCode()); assertTokenSignature(key2, response.getAccessToken()); assertTokenSignature(key2, response.getRefreshToken()); // Userinfo with keys #2 assertUserInfo(response.getAccessToken(), 200); // Token introspection with keys #2 assertTokenIntrospection(response.getAccessToken(), true); // Drop key #1 dropKeys1(); // Refresh token with keys #1 dropped - should pass as refresh token should be signed with key #2 response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password"); assertTokenSignature(key2, response.getAccessToken()); assertTokenSignature(key2, response.getRefreshToken()); // Userinfo with keys #1 dropped assertUserInfo(response.getAccessToken(), 200); // Token introspection with keys #1 dropped assertTokenIntrospection(response.getAccessToken(), true); // Drop key #2 dropKeys2(); // Userinfo with keys #2 dropped assertUserInfo(response.getAccessToken(), 401); // Token introspection with keys #2 dropped assertTokenIntrospection(response.getAccessToken(), false); // Refresh token with keys #2 dropped - should fail as refresh token is signed with key #2 response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password"); assertEquals(400, response.getStatusCode()); assertEquals("Invalid refresh token", response.getErrorDescription()); } @Test public void providerOrder() throws Exception { PublicKey keys1 = createKeys1(); PublicKey keys2 = createKeys2(); KeysMetadataRepresentation keyMetadata = adminClient.realm("test").keys().getKeyMetadata(); assertEquals(PemUtils.encodeKey(keys2), keyMetadata.getKeys().get(0).getPublicKey()); dropKeys1(); dropKeys2(); } @Test public void rotateKeys() throws InterruptedException { for (int i = 0; i < 10; i++) { String activeKid = adminClient.realm("test").keys().getKeyMetadata().getActive().get("RSA"); // Rotate public keys on the parent broker String realmId = adminClient.realm("test").toRepresentation().getId(); ComponentRepresentation keys = new ComponentRepresentation(); keys.setName("generated" + i); keys.setProviderType(KeyProvider.class.getName()); keys.setProviderId("rsa-generated"); keys.setParentId(realmId); keys.setConfig(new MultivaluedHashMap<>()); keys.getConfig().putSingle("priority", "1000" + i); Response response = adminClient.realm("test").components().add(keys); assertEquals(201, response.getStatus()); String newId = ApiUtil.getCreatedId(response); getCleanup().addComponentId(newId); response.close(); String updatedActiveKid = adminClient.realm("test").keys().getKeyMetadata().getActive().get("RSA"); assertNotEquals(activeKid, updatedActiveKid); } } static void assertTokenSignature(PublicKey expectedKey, String token) { String kid = null; try { RSATokenVerifier verifier = RSATokenVerifier.create(token).checkTokenType(false).checkRealmUrl(false).checkActive(false).publicKey(expectedKey); kid = verifier.getHeader().getKeyId(); verifier.verify(); } catch (VerificationException e) { fail("Token not signed by expected keys, kid was " + kid); } } private PublicKey createKeys1() throws Exception { return createKeys("1000"); } private PublicKey createKeys2() throws Exception { return createKeys("2000"); } private PublicKey createKeys(String priority) throws Exception { KeyPair keyPair = KeyUtils.generateRsaKeyPair(1024); String privateKeyPem = PemUtils.encodeKey(keyPair.getPrivate()); PublicKey publicKey = keyPair.getPublic(); ComponentRepresentation rep = new ComponentRepresentation(); rep.setName("mycomponent"); rep.setParentId("test"); rep.setProviderId(ImportedRsaKeyProviderFactory.ID); rep.setProviderType(KeyProvider.class.getName()); org.keycloak.common.util.MultivaluedHashMap config = new org.keycloak.common.util.MultivaluedHashMap(); config.addFirst("priority", priority); config.addFirst(Attributes.PRIVATE_KEY_KEY, privateKeyPem); rep.setConfig(config); Response response = adminClient.realm("test").components().add(rep); response.close(); rep = new ComponentRepresentation(); rep.setName("mycomponent2"); rep.setParentId("test"); rep.setProviderId(GeneratedHmacKeyProviderFactory.ID); rep.setProviderType(KeyProvider.class.getName()); config = new org.keycloak.common.util.MultivaluedHashMap(); config.addFirst("priority", priority); rep.setConfig(config); response = adminClient.realm("test").components().add(rep); response.close(); return publicKey; } private void dropKeys1() { dropKeys("1000"); } private void dropKeys2() { dropKeys("2000"); } private void dropKeys(String priority) { int r = 0; for (ComponentRepresentation c : adminClient.realm("test").components().query("test", KeyProvider.class.getName())) { if (c.getConfig().getFirst("priority").equals(priority)) { adminClient.realm("test").components().component(c.getId()).remove(); r++; } } if (r != 2) { throw new RuntimeException("Failed to find keys1"); } } private void assertUserInfo(String token, int expectedStatus) { Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(ClientBuilder.newClient(), token); assertEquals(expectedStatus, userInfoResponse.getStatus()); userInfoResponse.close(); } private void assertTokenIntrospection(String token, boolean expectActive) { try { String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", token); ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(tokenResponse); assertEquals(expectActive, jsonNode.get("active").asBoolean()); } catch (IOException e) { throw new RuntimeException(e); } } }