diff --git a/adapters/oidc/js/src/main/resources/keycloak.d.ts b/adapters/oidc/js/src/main/resources/keycloak.d.ts index fd306c7dd0..b97e301fdd 100644 --- a/adapters/oidc/js/src/main/resources/keycloak.d.ts +++ b/adapters/oidc/js/src/main/resources/keycloak.d.ts @@ -182,7 +182,8 @@ declare namespace Keycloak { interface KeycloakLoginOptions { /** - * @private Undocumented. + * Specifies the scope parameter for the login url + * The scope 'openid' will be added to the scope if it is missing or undefined. */ scope?: string; diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index 7f69f420bc..189741ac3d 100755 --- a/adapters/oidc/js/src/main/resources/keycloak.js +++ b/adapters/oidc/js/src/main/resources/keycloak.js @@ -191,6 +191,10 @@ } else { kc.enableLogging = false; } + + if (typeof initOptions.scope === "string") { + kc.scope = initOptions.scope; + } } if (!kc.responseMode) { @@ -433,15 +437,13 @@ baseUrl = kc.endpoints.authorize(); } - var scope; - if (options && options.scope) { - if (options.scope.indexOf("openid") != -1) { - scope = options.scope; - } else { - scope = "openid " + options.scope; - } - } else { + var scope = options && options.scope || kc.scope; + if (!scope) { + // if scope is not set, default to "openid" scope = "openid"; + } else if (scope.indexOf("openid") === -1) { + // if openid scope is missing, prefix the given scopes with it + scope = "openid " + scope; } var url = baseUrl diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java index ce0bf8dc06..40c05cd596 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java @@ -42,11 +42,11 @@ public class JavascriptTestExecutor { } public JavascriptTestExecutor login() { - return login(null, null); + return login((String)null, null); } public JavascriptTestExecutor login(JavascriptStateValidator validator) { - return login(null, validator); + return login((String)null, validator); } /** @@ -81,6 +81,10 @@ public class JavascriptTestExecutor { return this; } + public JavascriptTestExecutor login(JSObjectBuilder optionsBuilder, JavascriptStateValidator validator) { + return login(optionsBuilder.build(), validator); + } + public JavascriptTestExecutor login(String options, JavascriptStateValidator validator) { if (options == null) jsExecutor.executeScript("keycloak.login()"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java index 12c8428b42..ebd4199c78 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java @@ -30,6 +30,7 @@ import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.javascript.JSObjectBuilder; +import org.keycloak.testsuite.util.javascript.JavascriptStateValidator; import org.keycloak.testsuite.util.javascript.JavascriptTestExecutor; import org.keycloak.testsuite.util.javascript.XMLHttpRequest; import org.openqa.selenium.TimeoutException; @@ -450,6 +451,53 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest { .init(defaultArguments(), this::assertInitAuth); } + /** + * Test for scope handling via {@code initOptions}:
{@code
+     * Keycloak keycloak = new Keycloak(); keycloak.init({.... scope: "profile email phone"})
+     * }
+ * See KEYCLOAK-14412 + */ + @Test + public void testScopeInInitOptionsShouldBeConsideredByLoginUrl() { + + JSObjectBuilder initOptions = defaultArguments() + .loginRequiredOnLoad() + // phone is optional client scope + .add("scope", "profile email phone"); + + try { + testExecutor.init(initOptions); + // This throws exception because when JavascriptExecutor waits for AsyncScript to finish + // it is redirected to login page and executor gets no response + + throw new RuntimeException("Probably the login-required OnLoad mode doesn't work, because testExecutor should fail with error that page was redirected."); + } catch (WebDriverException ex) { + // should happen + } + + testExecutor.loginForm(testUser, this::assertOnTestAppUrl) + .init(initOptions, this::assertSuccessfullyLoggedIn) + .executeScript("return window.keycloak.tokenParsed.scope", assertOutputContains("phone")); + } + + /** + * Test for scope handling via {@code loginOptions}:
{@code
+     * Keycloak keycloak = new Keycloak(); keycloak.login({.... scope: "profile email phone"})
+     * }
+ * See KEYCLOAK-14412 + */ + @Test + public void testScopeInLoginOptionsShouldBeConsideredByLoginUrl() { + + testExecutor.configure().init(defaultArguments()); + + JSObjectBuilder loginOptions = JSObjectBuilder.create().add("scope", "profile email phone"); + + testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> { + assertThat(driver.getCurrentUrl(), containsString("&scope=openid%20profile%20email%20phone")); + }); + } + @Test public void testUpdateToken() { XMLHttpRequest request = XMLHttpRequest.create()