KEYCLOAK-12829 Require PKCE for admin and account console

This commit is contained in:
stianst 2020-02-10 13:48:08 +01:00 committed by Stian Thorgersen
parent 7969aed8e0
commit dda829710e
7 changed files with 80 additions and 8 deletions

View file

@ -57,6 +57,7 @@ public class MigrateTo9_0_0 implements Migration {
protected void migrateRealmCommon(RealmModel realm) {
addAccountConsoleClient(realm);
addAccountApiRoles(realm);
enablePkceAdminAccountClients(realm);
}
private void addAccountApiRoles(RealmModel realm) {
@ -100,4 +101,17 @@ public class MigrateTo9_0_0 implements Migration {
client.addProtocolMapper(audienceMapper);
}
}
private void enablePkceAdminAccountClients(RealmModel realm) {
ClientModel adminConsole = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
if (adminConsole != null) {
adminConsole.setAttribute("pkce.code.challenge.method", "S256");
}
ClientModel accountConsole = realm.getClientByClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID);
if (accountConsole != null) {
accountConsole.setAttribute("pkce.code.challenge.method", "S256");
}
}
}

View file

@ -41,6 +41,7 @@ import org.keycloak.models.utils.DefaultRequiredActions;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper;
@ -170,6 +171,8 @@ public class RealmManager {
adminConsole.setPublicClient(true);
adminConsole.setFullScopeAllowed(false);
adminConsole.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
adminConsole.setAttribute(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD, "S256");
}
protected void setupAdminConsoleLocaleMapper(RealmModel realm) {
@ -461,6 +464,8 @@ public class RealmManager {
audienceMapper.setProtocolMapper(AudienceResolveProtocolMapper.PROVIDER_ID);
accountConsoleClient.addProtocolMapper(audienceMapper);
accountConsoleClient.setAttribute(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD, "S256");
}
}
}

View file

@ -801,9 +801,8 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
@Test
public void openLoginFormWithDifferentApplication() throws Exception {
// Login form shown after redirect from admin console
oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console");
oauth.clientId("root-url-client");
oauth.redirectUri("http://localhost:8180/foo/bar/");
oauth.openLoginForm();
// Login form shown after redirect from app

View file

@ -41,6 +41,7 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
@ -281,6 +282,8 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testFirstBrokerLoginFlowMigrated(migrationRealm);
testAccountClient(masterRealm);
testAccountClient(migrationRealm);
testAdminClientPkce(masterRealm);
testAdminClientPkce(migrationRealm);
}
private void testAccountClient(RealmResource realm) {
@ -312,6 +315,11 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
assertEquals(1, adminConsoleClient.getWebOrigins().size());
}
private void testAdminClientPkce(RealmResource realm) {
ClientRepresentation adminConsoleClient = realm.clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0);
assertEquals("S256", adminConsoleClient.getAttributes().get(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD));
}
private void testAccountClientUrls(RealmResource realm) {
ClientRepresentation accountConsoleClient = realm.clients().findByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
@ -331,6 +339,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
assertFalse(accountConsoleClient.isFullScopeAllowed());
assertTrue(accountConsoleClient.isStandardFlowEnabled());
assertFalse(accountConsoleClient.isDirectAccessGrantsEnabled());
assertEquals("S256", accountConsoleClient.getAttributes().get(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD));
ClientResource clientResource = realm.clients().get(accountConsoleClient.getId());

View file

@ -238,8 +238,6 @@ public class AccessTokenTest extends AbstractKeycloakTest {
// KEYCLOAK-3692
@Test
public void accessTokenWrongCode() throws Exception {
oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console/nosuch.html");
oauth.openLoginForm();
String actionURI = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource());
@ -247,9 +245,9 @@ public class AccessTokenTest extends AbstractKeycloakTest {
oauth.fillLoginForm("test-user@localhost", "password");
events.expectLogin().client(Constants.ADMIN_CONSOLE_CLIENT_ID).detail(Details.REDIRECT_URI, AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console/nosuch.html").assertEvent();
events.expectLogin().assertEvent();
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(loginPageCode, null);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(loginPageCode, "password");
assertEquals(400, response.getStatusCode());
assertNull(response.getRefreshToken());

View file

@ -38,6 +38,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ActionURIUtils;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.oidc.PkceGenerator;
import org.keycloak.testsuite.runonserver.ServerVersion;
import java.io.IOException;
@ -65,9 +66,11 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
try (CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(cookieStore).build()) {
String redirectUri = URLEncoder.encode(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/admin/master/console", "UTF-8");
PkceGenerator pkce = new PkceGenerator();
HttpGet get = new HttpGet(
suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/auth?response_type=code&client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID +
"&redirect_uri=" + redirectUri);
"&redirect_uri=" + redirectUri + "&scope=openid&code_challenge_method=S256&code_challenge=" + pkce.getCodeChallenge());
CloseableHttpResponse response = client.execute(get);
String s = IOUtils.toString(response.getEntity().getContent(), "UTF-8");

View file

@ -0,0 +1,44 @@
package org.keycloak.testsuite.oidc;
import org.keycloak.common.util.Base64Url;
import java.security.MessageDigest;
import java.util.UUID;
public class PkceGenerator {
private String codeVerifier;
private String codeChallenge;
public PkceGenerator() {
codeVerifier = UUID.randomUUID().toString() + "-" + UUID.randomUUID().toString(); // Good enough for testing, but shouldn't be used elsewhere
codeChallenge = generateS256CodeChallenge(codeVerifier);
}
public PkceGenerator(String codeVerifier) {
this.codeVerifier = codeVerifier;
codeChallenge = generateS256CodeChallenge(codeVerifier);
}
public String getCodeVerifier() {
return codeVerifier;
}
public String getCodeChallenge() {
return codeChallenge;
}
private String generateS256CodeChallenge(String codeVerifier) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(codeVerifier.getBytes("ISO_8859_1"));
byte[] digestBytes = md.digest();
String codeChallenge = Base64Url.encode(digestBytes);
return codeChallenge;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}