KEYCLOAK-12829 Require PKCE for admin and account console
This commit is contained in:
parent
7969aed8e0
commit
dda829710e
7 changed files with 80 additions and 8 deletions
|
@ -57,6 +57,7 @@ public class MigrateTo9_0_0 implements Migration {
|
||||||
protected void migrateRealmCommon(RealmModel realm) {
|
protected void migrateRealmCommon(RealmModel realm) {
|
||||||
addAccountConsoleClient(realm);
|
addAccountConsoleClient(realm);
|
||||||
addAccountApiRoles(realm);
|
addAccountApiRoles(realm);
|
||||||
|
enablePkceAdminAccountClients(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addAccountApiRoles(RealmModel realm) {
|
private void addAccountApiRoles(RealmModel realm) {
|
||||||
|
@ -100,4 +101,17 @@ public class MigrateTo9_0_0 implements Migration {
|
||||||
client.addProtocolMapper(audienceMapper);
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import org.keycloak.models.utils.DefaultRequiredActions;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||||
import org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper;
|
import org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper;
|
||||||
|
@ -170,6 +171,8 @@ public class RealmManager {
|
||||||
adminConsole.setPublicClient(true);
|
adminConsole.setPublicClient(true);
|
||||||
adminConsole.setFullScopeAllowed(false);
|
adminConsole.setFullScopeAllowed(false);
|
||||||
adminConsole.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
adminConsole.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
|
|
||||||
|
adminConsole.setAttribute(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD, "S256");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setupAdminConsoleLocaleMapper(RealmModel realm) {
|
protected void setupAdminConsoleLocaleMapper(RealmModel realm) {
|
||||||
|
@ -461,6 +464,8 @@ public class RealmManager {
|
||||||
audienceMapper.setProtocolMapper(AudienceResolveProtocolMapper.PROVIDER_ID);
|
audienceMapper.setProtocolMapper(AudienceResolveProtocolMapper.PROVIDER_ID);
|
||||||
|
|
||||||
accountConsoleClient.addProtocolMapper(audienceMapper);
|
accountConsoleClient.addProtocolMapper(audienceMapper);
|
||||||
|
|
||||||
|
accountConsoleClient.setAttribute(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD, "S256");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -801,9 +801,8 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void openLoginFormWithDifferentApplication() throws Exception {
|
public void openLoginFormWithDifferentApplication() throws Exception {
|
||||||
// Login form shown after redirect from admin console
|
oauth.clientId("root-url-client");
|
||||||
oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
|
oauth.redirectUri("http://localhost:8180/foo/bar/");
|
||||||
oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console");
|
|
||||||
oauth.openLoginForm();
|
oauth.openLoginForm();
|
||||||
|
|
||||||
// Login form shown after redirect from app
|
// Login form shown after redirect from app
|
||||||
|
|
|
@ -41,6 +41,7 @@ import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
import org.keycloak.models.utils.TimeBasedOTP;
|
import org.keycloak.models.utils.TimeBasedOTP;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
|
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
|
||||||
|
@ -281,6 +282,8 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
|
||||||
testFirstBrokerLoginFlowMigrated(migrationRealm);
|
testFirstBrokerLoginFlowMigrated(migrationRealm);
|
||||||
testAccountClient(masterRealm);
|
testAccountClient(masterRealm);
|
||||||
testAccountClient(migrationRealm);
|
testAccountClient(migrationRealm);
|
||||||
|
testAdminClientPkce(masterRealm);
|
||||||
|
testAdminClientPkce(migrationRealm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testAccountClient(RealmResource realm) {
|
private void testAccountClient(RealmResource realm) {
|
||||||
|
@ -312,6 +315,11 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
|
||||||
assertEquals(1, adminConsoleClient.getWebOrigins().size());
|
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) {
|
private void testAccountClientUrls(RealmResource realm) {
|
||||||
ClientRepresentation accountConsoleClient = realm.clients().findByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
|
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());
|
assertFalse(accountConsoleClient.isFullScopeAllowed());
|
||||||
assertTrue(accountConsoleClient.isStandardFlowEnabled());
|
assertTrue(accountConsoleClient.isStandardFlowEnabled());
|
||||||
assertFalse(accountConsoleClient.isDirectAccessGrantsEnabled());
|
assertFalse(accountConsoleClient.isDirectAccessGrantsEnabled());
|
||||||
|
assertEquals("S256", accountConsoleClient.getAttributes().get(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD));
|
||||||
|
|
||||||
ClientResource clientResource = realm.clients().get(accountConsoleClient.getId());
|
ClientResource clientResource = realm.clients().get(accountConsoleClient.getId());
|
||||||
|
|
||||||
|
|
|
@ -238,8 +238,6 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||||
// KEYCLOAK-3692
|
// KEYCLOAK-3692
|
||||||
@Test
|
@Test
|
||||||
public void accessTokenWrongCode() throws Exception {
|
public void accessTokenWrongCode() throws Exception {
|
||||||
oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
|
|
||||||
oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console/nosuch.html");
|
|
||||||
oauth.openLoginForm();
|
oauth.openLoginForm();
|
||||||
|
|
||||||
String actionURI = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource());
|
String actionURI = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource());
|
||||||
|
@ -247,9 +245,9 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
oauth.fillLoginForm("test-user@localhost", "password");
|
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());
|
assertEquals(400, response.getStatusCode());
|
||||||
assertNull(response.getRefreshToken());
|
assertNull(response.getRefreshToken());
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.ActionURIUtils;
|
import org.keycloak.testsuite.ActionURIUtils;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
|
import org.keycloak.testsuite.oidc.PkceGenerator;
|
||||||
import org.keycloak.testsuite.runonserver.ServerVersion;
|
import org.keycloak.testsuite.runonserver.ServerVersion;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -65,9 +66,11 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
|
||||||
try (CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(cookieStore).build()) {
|
try (CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(cookieStore).build()) {
|
||||||
String redirectUri = URLEncoder.encode(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/admin/master/console", "UTF-8");
|
String redirectUri = URLEncoder.encode(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/admin/master/console", "UTF-8");
|
||||||
|
|
||||||
|
PkceGenerator pkce = new PkceGenerator();
|
||||||
|
|
||||||
HttpGet get = new HttpGet(
|
HttpGet get = new HttpGet(
|
||||||
suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/auth?response_type=code&client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID +
|
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);
|
CloseableHttpResponse response = client.execute(get);
|
||||||
String s = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
|
String s = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue