diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index 2d8f4218c8..01f0523511 100755 --- a/adapters/oidc/js/src/main/resources/keycloak.js +++ b/adapters/oidc/js/src/main/resources/keycloak.js @@ -29,7 +29,7 @@ var loginIframe = { enable: true, - callbackMap: [], + callbackList: [], interval: 5 }; @@ -824,7 +824,7 @@ setTimeout(check, loginIframe.interval * 1000); } - var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html?client_id=' + encodeURIComponent(kc.clientId) + '&origin=' + getOrigin(); + var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; iframe.setAttribute('src', src ); iframe.style.display = 'none'; document.body.appendChild(iframe); @@ -834,30 +834,21 @@ return; } - try { - var data = JSON.parse(event.data); - } catch (err) { - return; - } - - if (!data.callbackId) { - return; - } - - var promise = loginIframe.callbackMap[data.callbackId]; - if (!promise) { - return; - } - - delete loginIframe.callbackMap[data.callbackId]; - - if ((!kc.sessionId || kc.sessionId == data.session) && data.loggedIn) { - promise.setSuccess(); - } else { + if (event.data != "unchanged") { kc.clearToken(); - promise.setError(); + } + + for (var i = loginIframe.callbackList.length - 1; i >= 0; --i) { + var promise = loginIframe.callbackList[i]; + if (event.data == "unchanged") { + promise.setSuccess(); + } else { + promise.setError(); + } + loginIframe.callbackList.splice(i, 1); } }; + window.addEventListener('message', messageCallback, false); var check = function() { @@ -873,12 +864,13 @@ function checkLoginIframe() { var promise = createPromise(); - if (loginIframe.iframe && loginIframe.iframeOrigin) { - var msg = {}; - msg.callbackId = createCallbackId(); - loginIframe.callbackMap[msg.callbackId] = promise; + if (loginIframe.iframe && loginIframe.iframeOrigin ) { + var msg = kc.clientId + ' ' + kc.sessionId; + loginIframe.callbackList.push(promise); var origin = loginIframe.iframeOrigin; - loginIframe.iframe.contentWindow.postMessage(JSON.stringify(msg), origin); + if (loginIframe.callbackList.length == 1) { + loginIframe.iframe.contentWindow.postMessage(msg, origin); + } } else { promise.setSuccess(); } diff --git a/adapters/oidc/js/src/main/resources/login-status-iframe.html b/adapters/oidc/js/src/main/resources/login-status-iframe.html index fe7eda1cc5..8794d54795 100755 --- a/adapters/oidc/js/src/main/resources/login-status-iframe.html +++ b/adapters/oidc/js/src/main/resources/login-status-iframe.html @@ -15,10 +15,56 @@ ~ limitations under the License. --> + + \ No newline at end of file + + + \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index c2f4296769..0485e3f11d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -90,6 +90,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setUserinfoEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); config.setLogoutEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); config.setJwksUri(uriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setCheckSessionIframe(uriBuilder.clone().path(OIDCLoginProtocolService.class, "getLoginStatusIframe").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString()); config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java index b41f52d721..c46ba108d3 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java @@ -17,27 +17,25 @@ package org.keycloak.protocol.oidc.endpoints; -import org.jboss.resteasy.spi.NotFoundException; -import org.keycloak.Config; -import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.UriUtils; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.utils.WebOriginsUtils; +import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.P3PHelper; import javax.ws.rs.GET; +import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.io.IOException; import java.io.InputStream; +import java.util.Set; /** * @author Stian Thorgersen @@ -58,62 +56,40 @@ public class LoginStatusIframeEndpoint { @GET @Produces(MediaType.TEXT_HTML) - public Response getLoginStatusIframe(@QueryParam("client_id") String client_id, - @QueryParam("origin") String origin) { - if (client_id == null || origin == null) { - throw new WebApplicationException(Response.Status.BAD_REQUEST); - } - - if (!UriUtils.isOrigin(origin)) { - throw new WebApplicationException(Response.Status.BAD_REQUEST); - } - - ClientModel client = realm.getClientByClientId(client_id); - if (client == null) { - throw new WebApplicationException(Response.Status.BAD_REQUEST); - } - - InputStream is = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html"); - if (is == null) throw new NotFoundException("Could not find login-status-iframe.html "); - - boolean valid = false; - for (String o : client.getWebOrigins()) { - if (o.equals("*") || o.equals(origin)) { - valid = true; - break; - } - } - - for (String r : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) { - int i = r.indexOf('/', 8); - if (i != -1) { - r = r.substring(0, i); - } - - if (r.equals(origin)) { - valid = true; - break; - } - } - - if (!valid) { - throw new WebApplicationException(Response.Status.BAD_REQUEST); - } - - try { - String file = StreamUtil.readString(is); - file = file.replace("ORIGIN", origin); - + public Response getLoginStatusIframe() { + InputStream resource = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html"); + if (resource != null) { P3PHelper.addP3PHeader(session); - - CacheControl cacheControl = new CacheControl(); - cacheControl.setNoTransform(false); - cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1)); - - return Response.ok(file).cacheControl(cacheControl).build(); - } catch (IOException e) { - throw new WebApplicationException(e, Response.Status.BAD_REQUEST); + return Response.ok(resource).type(MediaType.TEXT_HTML_TYPE).cacheControl(CacheControlUtil.getDefaultCacheControl()).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); } } + @GET + @Path("init") + public Response preCheck(@QueryParam("client_id") String clientId, @QueryParam("origin") String origin, @QueryParam("session_state") String sessionState) { + try { + RealmModel realm = session.getContext().getRealm(); + String sessionId = sessionState.split("/")[2]; + UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); + if (userSession == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + ClientModel client = session.realms().getClientByClientId(clientId, realm); + if (client != null) { + Set validWebOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client); + validWebOrigins.add(UriUtils.getOrigin(uriInfo.getRequestUri())); + + if (validWebOrigins.contains(origin)) { + return Response.noContent().build(); + } + } + } catch (Throwable t) { + } + + return Response.status(Response.Status.FORBIDDEN).build(); + } + } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index 181e0d28df..cb94c1cab0 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -52,6 +52,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("jwks_uri") private String jwksUri; + @JsonProperty("check_session_iframe") + private String checkSessionIframe; + @JsonProperty("grant_types_supported") private List grantTypesSupported; @@ -150,6 +153,14 @@ public class OIDCConfigurationRepresentation { this.jwksUri = jwksUri; } + public String getCheckSessionIframe() { + return checkSessionIframe; + } + + public void setCheckSessionIframe(String checkSessionIframe) { + this.checkSessionIframe = checkSessionIframe; + } + public String getLogoutEndpoint() { return logoutEndpoint; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java index 437e7d88ab..2d0a269c8e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java @@ -53,7 +53,7 @@ import static org.junit.Assert.assertTrue; public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest { @Test - public void checkIframeP3PHeader() throws IOException { + public void checkIframe() throws IOException { CookieStore cookieStore = new BasicCookieStore(); CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(cookieStore).build(); @@ -115,17 +115,66 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest { } assertNotNull(sessionCookie); - get = new HttpGet( - suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html?client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot()); + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html"); response = client.execute(get); assertEquals(200, response.getStatusLine().getStatusCode()); s = IOUtils.toString(response.getEntity().getContent()); - assertTrue(s.contains("function getCookie(cname)")); + assertTrue(s.contains("function getCookie()")); assertEquals("CP=\"This is not a P3P policy!\"", response.getFirstHeader("P3P").getValue()); response.close(); + + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init"); + response = client.execute(get); + assertEquals(403, response.getStatusLine().getStatusCode()); + response.close(); + + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?" + + "client_id=invalid" + + "&session_state=" + sessionCookie.getValue() + + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot() + ); + response = client.execute(get); + assertEquals(403, response.getStatusLine().getStatusCode()); + response.close(); + + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?" + + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID + + "&session_state=invalid" + + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot() + ); + response = client.execute(get); + assertEquals(403, response.getStatusLine().getStatusCode()); + response.close(); + + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?" + + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID + + "&session_state=" + sessionCookie.getValue() + + "&origin=http://invalid" + ); + response = client.execute(get); + assertEquals(403, response.getStatusLine().getStatusCode()); + response.close(); + + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?" + + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID + + "&session_state=master/random/random" + + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot() + ); + response = client.execute(get); + assertEquals(404, response.getStatusLine().getStatusCode()); + response.close(); + + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?" + + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID + + "&session_state=" + sessionCookie.getValue() + + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot() + ); + response = client.execute(get); + assertEquals(204, response.getStatusLine().getStatusCode()); + response.close(); } finally { client.close(); }