From 4dfa2249ebb6a75dde4b030bf3b6daacda7a5fe6 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 27 Nov 2024 10:27:39 -0300 Subject: [PATCH] Allow asking for additional scopes when querying the account console root URL Closes #35243 Signed-off-by: Pedro Igor --- .../server_admin/topics/account.adoc | 4 ++ .../resources/account/AccountConsole.java | 4 ++ .../testsuite/account/AccountConsoleTest.java | 62 +++++++++++++++---- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/docs/documentation/server_admin/topics/account.adoc b/docs/documentation/server_admin/topics/account.adoc index cea503e8632a..f27ae28e84cd 100644 --- a/docs/documentation/server_admin/topics/account.adoc +++ b/docs/documentation/server_admin/topics/account.adoc @@ -20,6 +20,10 @@ .Account Console image:images/account-console-intro.png[Account Console] +You can also ask for additional scopes when calling the account console URL by setting the `scope` parameter in this format: _server-root_{kc_realms_path}/{realm-name}/account?scope=phone. +The scopes can only be requested (and eventually granted) when the user is authenticating to the account console. Once authenticated, the granted scopes won't change +until re-authenticating. + === Configuring ways to sign in You can sign in to this console using basic authentication (a login name and password) or two-factor authentication. For two-factor authentication, use one of the following procedures. diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java index 49ce332c35d8..029bd9634640 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java @@ -271,6 +271,10 @@ private Response redirectToLogin(String path) { throw new ServerErrorException(Status.INTERNAL_SERVER_ERROR); } } + String scope = queryParameters.getFirst(OIDCLoginProtocol.SCOPE_PARAM); + if (StringUtil.isNotBlank(scope)) { + uriBuilder.queryParam(OIDCLoginProtocol.SCOPE_PARAM, scope); + } } URI url = uriBuilder.build(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountConsoleTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountConsoleTest.java index 6a98f58921a6..940878bffd14 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountConsoleTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountConsoleTest.java @@ -1,15 +1,21 @@ package org.keycloak.testsuite.account; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URLDecoder; + import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.HttpClientBuilder; import org.jboss.arquillian.graphene.page.Page; import org.junit.Test; +import org.keycloak.models.Constants; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.util.OAuthClient; public class AccountConsoleTest extends AbstractTestRealmKeycloakTest { @@ -22,23 +28,53 @@ public void configureTestRealm(RealmRepresentation testRealm) { } @Test - public void redirectToLoginIfNotAuthenticated() throws Exception { + public void redirectToLoginIfNotAuthenticated() { + assertRedirectLocation(getAccount()); + } + + @Test + public void testScopesPresentInAuthorizationRequest() { + String expectedScopes = "phone address"; + String redirectLocation = URLDecoder.decode(assertRedirectLocation(getAccount(expectedScopes))); + Assert.assertTrue(redirectLocation.contains(expectedScopes)); + expectedScopes = "phone"; + redirectLocation = URLDecoder.decode(assertRedirectLocation(getAccount(expectedScopes))); + Assert.assertTrue(redirectLocation.contains(expectedScopes)); + Assert.assertTrue(!redirectLocation.contains("address")); + } - String accountUrl = oauth.getCurrentUri().toString().replace("/admin/master/console", "/realms/" + oauth.getRealm() + "/account"); + private CloseableHttpResponse getAccount() { + return getAccount(null); + } - HttpGet getAccount = new HttpGet(accountUrl); + private CloseableHttpResponse getAccount(String scope) { + try { + var uriBuilder = new URIBuilder(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account"); - int statusCode; - String redirectLocation; - try (var client = HttpClientBuilder.create().disableRedirectHandling().build()) { - try (var response = client.execute(getAccount)) { - statusCode = response.getStatusLine().getStatusCode(); - redirectLocation = response.getFirstHeader("Location").getValue(); + if (scope != null) { + uriBuilder.setParameter(OIDCLoginProtocol.SCOPE_PARAM, scope); } + + var request = new HttpGet(uriBuilder.build()); + + try (var client = HttpClientBuilder.create().disableRedirectHandling().build()) { + return client.execute(request); + } + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); } + } - Assert.assertEquals(302, statusCode); - String expectedLoginUrlPart = "/realms/" + oauth.getRealm() + "/protocol/openid-connect/auth?client_id=account"; - Assert.assertTrue(redirectLocation.contains(expectedLoginUrlPart)); + private String assertRedirectLocation(CloseableHttpResponse Account) { + try (var response = Account) { + int statusCode = response.getStatusLine().getStatusCode(); + Assert.assertEquals(302, statusCode); + String expectedLoginUrlPart = "/realms/" + oauth.getRealm() + "/protocol/openid-connect/auth?client_id=" + Constants.ACCOUNT_CONSOLE_CLIENT_ID; + String redirectLocation = response.getFirstHeader("Location").getValue(); + Assert.assertTrue(redirectLocation.contains(expectedLoginUrlPart)); + return redirectLocation; + } catch (IOException e) { + throw new RuntimeException(e); + } } }