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 4448aa11fd..757f0d769c 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 @@ -20,10 +20,12 @@ package org.keycloak.protocol.oidc.endpoints; import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.Config; 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.common.util.StreamUtil; import org.keycloak.common.util.UriUtils; +import org.keycloak.services.util.P3PHelper; import javax.ws.rs.GET; import javax.ws.rs.Produces; @@ -45,6 +47,9 @@ public class LoginStatusIframeEndpoint { @Context private UriInfo uriInfo; + @Context + private KeycloakSession session; + private RealmModel realm; public LoginStatusIframeEndpoint(RealmModel realm) { @@ -99,6 +104,8 @@ public class LoginStatusIframeEndpoint { String file = StreamUtil.readString(is); file = file.replace("ORIGIN", origin); + P3PHelper.addP3PHeader(session); + CacheControl cacheControl = new CacheControl(); cacheControl.setNoTransform(false); cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1)); diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java index 29e0f5690f..d9a57cf765 100755 --- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java @@ -36,7 +36,7 @@ public class AppAuthManager extends AuthenticationManager { AuthResult authResult = super.authenticateIdentityCookie(session, realm); if (authResult == null) return null; // refresh the cookies! - createLoginCookie(realm, authResult.getUser(), authResult.getSession(), session.getContext().getUri(), session.getContext().getConnection()); + createLoginCookie(session, realm, authResult.getUser(), authResult.getSession(), session.getContext().getUri(), session.getContext().getConnection()); if (authResult.getSession().isRememberMe()) createRememberMeCookie(realm, authResult.getUser().getUsername(), session.getContext().getUri(), session.getContext().getConnection()); return authResult; } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 33ed98e163..c2c774c071 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -45,6 +45,7 @@ import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.util.CookieHelper; import org.keycloak.common.util.Time; +import org.keycloak.services.util.P3PHelper; import javax.ws.rs.core.*; import java.net.URI; @@ -259,7 +260,7 @@ public class AuthenticationManager { return token; } - public static void createLoginCookie(RealmModel realm, UserModel user, UserSessionModel session, UriInfo uriInfo, ClientConnection connection) { + public static void createLoginCookie(KeycloakSession keycloakSession, RealmModel realm, UserModel user, UserSessionModel session, UriInfo uriInfo, ClientConnection connection) { String cookiePath = getIdentityCookiePath(realm, uriInfo); String issuer = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()); AccessToken identityToken = createIdentityToken(realm, user, session, issuer); @@ -280,7 +281,7 @@ public class AuthenticationManager { // THIS SHOULD NOT BE A HTTPONLY COOKIE! It is used for OpenID Connect Iframe Session support! // Max age should be set to the max lifespan of the session as it's used to invalidate old-sessions on re-login CookieHelper.addCookie(KEYCLOAK_SESSION_COOKIE, sessionCookieValue, cookiePath, null, null, realm.getSsoSessionMaxLifespan(), secureOnly, false); - + P3PHelper.addP3PHeader(keycloakSession); } public static void createRememberMeCookie(RealmModel realm, String username, UriInfo uriInfo, ClientConnection connection) { @@ -399,7 +400,7 @@ public class AuthenticationManager { session.getContext().resolveLocale(userSession.getUser()); // refresh the cookies! - createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection); + createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection); if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN); if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection); return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession)); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index e413c72d47..5427a1a665 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -330,7 +330,7 @@ public class UsersResource { EventBuilder event = new EventBuilder(realm, session, clientConnection); UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null); - AuthenticationManager.createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection); + AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection); URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName()); Map result = new HashMap<>(); result.put("sameRealm", sameRealm); diff --git a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java index 18711931c6..d8a7c80624 100755 --- a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java +++ b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java @@ -16,6 +16,7 @@ */ package org.keycloak.services.util; +import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -44,6 +45,25 @@ public class LocaleHelper { } } + public static Locale getLocaleFromCookie(KeycloakSession session) { + KeycloakContext ctx = session.getContext(); + + if (ctx.getRequestHeaders() != null && ctx.getRequestHeaders().getCookies().containsKey(LOCALE_COOKIE)) { + String localeString = ctx.getRequestHeaders().getCookies().get(LOCALE_COOKIE).getValue(); + Locale locale = findLocale(ctx.getRealm().getSupportedLocales(), localeString); + if (locale != null) { + return locale; + } + } + + String locale = ctx.getRealm().getDefaultLocale(); + if (locale != null) { + return Locale.forLanguageTag(locale); + } else { + return Locale.ENGLISH; + } + } + private static Locale getUserLocale(KeycloakSession session, RealmModel realm, UserModel user) { UriInfo uriInfo = session.getContext().getUri(); HttpHeaders httpHeaders = session.getContext().getRequestHeaders(); diff --git a/services/src/main/java/org/keycloak/services/util/P3PHelper.java b/services/src/main/java/org/keycloak/services/util/P3PHelper.java new file mode 100644 index 0000000000..84e2e21bbd --- /dev/null +++ b/services/src/main/java/org/keycloak/services/util/P3PHelper.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.services.util; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpResponse; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.validation.Validation; +import org.keycloak.theme.Theme; +import org.keycloak.theme.ThemeProvider; + +import java.io.IOException; +import java.util.Locale; + +/** + * IE requires P3P header to allow loading cookies from iframes when domain differs from main page (see KEYCLOAK-2828 for more details) + * + * @author Stian Thorgersen + */ +public class P3PHelper { + + private static final Logger logger = Logger.getLogger(P3PHelper.class); + + public static void addP3PHeader(KeycloakSession session) { + try { + ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); + Theme theme = themeProvider.getTheme(session.getContext().getRealm().getLoginTheme(), Theme.Type.LOGIN); + + Locale locale = LocaleHelper.getLocaleFromCookie(session); + String p3pValue = theme.getMessages(locale).getProperty("p3pPolicy"); + + if (!Validation.isBlank(p3pValue)) { + HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class); + response.getOutputHeaders().putSingle("P3P", p3pValue); + } + } catch (IOException e) { + logger.error("Failed to set P3P header", e); + return; + } + } + +} 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 new file mode 100644 index 0000000000..5386141534 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.oauth; + +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.NameValuePair; +import org.apache.http.client.CookieStore; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.junit.Test; +import org.keycloak.models.Constants; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.*; + +/** + * @author Stian Thorgersen + */ +public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest { + + @Test + public void checkIframeP3PHeader() throws IOException { + CookieStore cookieStore = new BasicCookieStore(); + + CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(cookieStore).build(); + try { + HttpGet get = new HttpGet( + suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/auth?response_type=code&client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID); + + CloseableHttpResponse response = client.execute(get); + String s = IOUtils.toString(response.getEntity().getContent()); + response.close(); + + Matcher matcher = Pattern.compile("action=\"([^\"]*)\"").matcher(s); + matcher.find(); + + String action = matcher.group(1); + + HttpPost post = new HttpPost(action); + + List params = new LinkedList<>(); + params.add(new BasicNameValuePair("username", "admin")); + params.add(new BasicNameValuePair("password", "admin")); + + post.setHeader("Content-Type", "application/x-www-form-urlencoded"); + post.setEntity(new UrlEncodedFormEntity(params)); + + response = client.execute(post); + + assertEquals("CP=\"This is not a P3P policy!\"", response.getFirstHeader("P3P").getValue()); + + Header setIdentityCookieHeader = null; + Header setSessionCookieHeader = null; + for (Header h : response.getAllHeaders()) { + if (h.getName().equals("Set-Cookie")) { + if (h.getValue().contains("KEYCLOAK_SESSION")) { + setSessionCookieHeader = h; + + } else if (h.getValue().contains("KEYCLOAK_IDENTITY")) { + setIdentityCookieHeader = h; + } + } + } + assertNotNull(setIdentityCookieHeader); + assertTrue(setIdentityCookieHeader.getValue().contains("HttpOnly")); + + assertNotNull(setSessionCookieHeader); + assertFalse(setSessionCookieHeader.getValue().contains("HttpOnly")); + + response.close(); + + Cookie sessionCookie = null; + for (Cookie cookie : cookieStore.getCookies()) { + if (cookie.getName().equals("KEYCLOAK_SESSION")) { + sessionCookie = cookie; + break; + } + } + 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()); + response = client.execute(get); + + assertEquals(200, response.getStatusLine().getStatusCode()); + s = IOUtils.toString(response.getEntity().getContent()); + assertTrue(s.contains("function getCookie(cname)")); + + assertEquals("CP=\"This is not a P3P policy!\"", response.getFirstHeader("P3P").getValue()); + + response.close(); + } finally { + client.close(); + } + } + + @Override + public void addTestRealms(List testRealms) { + } + +} diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 039e64981c..d0ffefc5d5 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -223,3 +223,4 @@ clientNotFoundMessage=Client not found. invalidParameterMessage=Invalid parameter\: {0} alreadyLoggedIn=You are already logged in. +p3pPolicy=CP="This is not a P3P policy!" \ No newline at end of file