diff --git a/adapters/oidc/js/src/main/resources/keycloak.d.ts b/adapters/oidc/js/src/main/resources/keycloak.d.ts index b7a5dcaca6..ae41ce7b88 100644 --- a/adapters/oidc/js/src/main/resources/keycloak.d.ts +++ b/adapters/oidc/js/src/main/resources/keycloak.d.ts @@ -106,6 +106,13 @@ declare namespace Keycloak { */ redirectUri?: string; + /** + * Specifies an uri to redirect to after silent check-sso. + * Silent check-sso will only happen, when this redirect uri is given and + * the specified uri is available whithin the application. + */ + silentCheckSsoRedirectUri?: string; + /** * Set the OpenID Connect flow. * @default standard diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index 2ef8e305a0..decc10b609 100755 --- a/adapters/oidc/js/src/main/resources/keycloak.js +++ b/adapters/oidc/js/src/main/resources/keycloak.js @@ -116,6 +116,10 @@ kc.redirectUri = initOptions.redirectUri; } + if (initOptions.silentCheckSsoRedirectUri) { + kc.silentCheckSsoRedirectUri = initOptions.silentCheckSsoRedirectUri; + } + if (initOptions.pkceMethod) { if (initOptions.pkceMethod !== "S256") { throw 'Invalid value for pkceMethod'; @@ -157,6 +161,29 @@ }); } + var checkSsoSilently = function() { + var ifrm = document.createElement("iframe"); + var src = kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri}); + ifrm.setAttribute("src", src); + ifrm.setAttribute("title", "keycloak-silent-check-sso"); + ifrm.style.display = "none"; + document.body.appendChild(ifrm); + + var messageCallback = function(event) { + if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) { + return; + } + + var oauth = parseCallback(event.data); + processCallback(oauth, initPromise); + + document.body.removeChild(ifrm); + window.removeEventListener("message", messageCallback); + }; + + window.addEventListener("message", messageCallback); + }; + var options = {}; switch (initOptions.onLoad) { case 'check-sso': @@ -164,7 +191,7 @@ setupCheckLoginIframe().success(function() { checkLoginIframe().success(function (unchanged) { if (!unchanged) { - doLogin(false); + kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false); } else { initPromise.setSuccess(); } @@ -173,7 +200,7 @@ }); }); } else { - doLogin(false); + kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false); } break; case 'login-required': diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java index 2986923c75..c491d60255 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java @@ -30,6 +30,13 @@ public class TestJavascriptResource { return resourceToString("/javascript/index.html"); } + @GET + @Path("/silent-check-sso.html") + @Produces(MediaType.TEXT_HTML) + public String getJavascriptTestingEnvironmentSilentCheckSso() throws IOException { + return resourceToString("/javascript/silent-check-sso.html"); + } + @GET @Path("/keycloak.json") @Produces(MediaType.APPLICATION_JSON) diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/silent-check-sso.html b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/silent-check-sso.html new file mode 100644 index 0000000000..60a7af916a --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/silent-check-sso.html @@ -0,0 +1 @@ +
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JSObjectBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JSObjectBuilder.java index f5bd2b8d02..930e50d5dd 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JSObjectBuilder.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JSObjectBuilder.java @@ -78,6 +78,10 @@ public class JSObjectBuilder { return this; } + private boolean skipQuotes(Object o) { + return (o instanceof Integer || o instanceof Boolean); + } + public String build() { StringBuilder argument = new StringBuilder("{"); String comma = ""; @@ -86,11 +90,11 @@ public class JSObjectBuilder { .append(option.getKey()) .append(" : "); - if (!(option.getValue() instanceof Integer)) argument.append("\""); + if (!skipQuotes(option.getValue())) argument.append("\""); argument.append(option.getValue()); - if (!(option.getValue() instanceof Integer)) argument.append("\""); + if (!skipQuotes(option.getValue())) argument.append("\""); comma = ","; } 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 0bbecd11cc..f38ebcd326 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 @@ -47,6 +47,7 @@ import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; @@ -153,6 +154,58 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest { .init(pkceS256, this::assertInitNotAuth); } + @Test + public void testSilentCheckSso() { + JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad(); + testExecutor.init(checkSSO, this::assertInitNotAuth) + .login(this::assertOnLoginPage) + .loginForm(testUser, this::assertOnTestAppUrl) + .init(checkSSO, this::assertSuccessfullyLoggedIn) + .refresh() + .init(checkSSO + .add("silentCheckSsoRedirectUri", authServerContextRootPage + JAVASCRIPT_URL + "/silent-check-sso.html") + , this::assertSuccessfullyLoggedIn); + } + + @Test + public void testSilentCheckSsoLoginWithLoginIframeDisabled() { + JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad(); + testExecutor.init(checkSSO, this::assertInitNotAuth) + .login(this::assertOnLoginPage) + .loginForm(testUser, this::assertOnTestAppUrl) + .init(checkSSO, this::assertSuccessfullyLoggedIn) + .refresh() + .init(checkSSO + .add("checkLoginIframe", false) + .add("silentCheckSsoRedirectUri", authServerContextRootPage + JAVASCRIPT_URL + "/silent-check-sso.html") + , this::assertSuccessfullyLoggedIn); + } + + @Test + public void testSilentCheckSsoWithoutRedirectUri() { + JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad(); + try { + testExecutor.init(checkSSO, this::assertInitNotAuth) + .login(this::assertOnLoginPage) + .loginForm(testUser, this::assertOnTestAppUrl) + .init(checkSSO, this::assertSuccessfullyLoggedIn) + .refresh() + .init(checkSSO); + fail(); + } catch (WebDriverException e) { + // should happen + } + } + + @Test + public void testSilentCheckSsoNotAuthenticated() { + JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad(); + testExecutor.init(checkSSO + .add("checkLoginIframe", false) + .add("silentCheckSsoRedirectUri", authServerContextRootPage + JAVASCRIPT_URL + "/silent-check-sso.html") + , this::assertInitNotAuth); + } + @Test public void testRefreshToken() { testExecutor.init(defaultArguments(), this::assertInitNotAuth)