diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java index f8d72783f6..56d8f51298 100755 --- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java +++ b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java @@ -18,6 +18,8 @@ package org.keycloak.adapters.jetty.core; import org.eclipse.jetty.security.DefaultUserIdentity; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.ServerAuthException; import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.security.authentication.DeferredAuthentication; @@ -135,10 +137,44 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica return new DefaultUserIdentity(theSubject, principal, theRoles); } + private class DummyLoginService implements LoginService { + @Override + public String getName() { + return null; + } + + @Override + public UserIdentity login(String username, Object credentials) { + return null; + } + + @Override + public boolean validate(UserIdentity user) { + return false; + } + + @Override + public IdentityService getIdentityService() { + return null; + } + + @Override + public void setIdentityService(IdentityService service) { + + } + + @Override + public void logout(UserIdentity user) { + + } + } + @Override public void setConfiguration(AuthConfiguration configuration) { //super.setConfiguration(configuration); initializeKeycloak(); + // need this so that getUserPrincipal does not throw NPE + _loginService = new DummyLoginService(); String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); setErrorPage(error); } diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java index 8a3010ddd9..70a67de8a5 100755 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java +++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java @@ -89,6 +89,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte protected void cleanSession(HttpSession session) { session.removeAttribute(KeycloakAccount.class.getName()); + session.removeAttribute(KeycloakSecurityContext.class.getName()); clearSavedRequest(session); } @@ -160,6 +161,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext); HttpSession httpSession = request.getSession(); httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount); + httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext()); if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId()); //String username = securityContext.getToken().getSubject(); //log.fine("userSessionManagement.login: " + username); diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java index 7734c2d5d5..0a07e9e6a3 100755 --- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java +++ b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java @@ -69,12 +69,22 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple // just in case session got serialized if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; + if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) { + request.setAttribute(KeycloakSecurityContext.class.getName(), session); + request.setUserPrincipal(account.getPrincipal()); + request.setAuthType("KEYCLOAK"); + return; + } // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will // not be updated boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) return; + if (success && session.isActive()) { + request.setAttribute(KeycloakSecurityContext.class.getName(), session); + request.setUserPrincipal(account.getPrincipal()); + request.setAuthType("KEYCLOAK"); + return; + } // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session log.fine("Cleanup and expire session " + catalinaSession.getId() + " after failed refresh"); @@ -85,6 +95,8 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple } protected void cleanSession(Session catalinaSession) { + catalinaSession.getSession().removeAttribute(KeycloakSecurityContext.class.getName()); + catalinaSession.getSession().removeAttribute(SerializableKeycloakAccount.class.getName()); catalinaSession.getSession().removeAttribute(OidcKeycloakAccount.class.getName()); catalinaSession.setPrincipal(null); catalinaSession.setAuthType(null); @@ -164,6 +176,7 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple session.setPrincipal(principal); session.setAuthType("KEYCLOAK"); session.getSession().setAttribute(SerializableKeycloakAccount.class.getName(), sAccount); + session.getSession().setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); String username = securityContext.getToken().getSubject(); log.fine("userSessionManagement.login: " + username); this.sessionManagement.login(session); diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java index 63c27d78da..5db3eadca3 100755 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java +++ b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java @@ -92,7 +92,8 @@ public class ServletSessionTokenStore implements AdapterTokenStore { } else { log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session"); try { - session.setAttribute(KeycloakUndertowAccount.class.getName(), null); + session.removeAttribute(KeycloakUndertowAccount.class.getName()); + session.removeAttribute(KeycloakSecurityContext.class.getName()); session.invalidate(); } catch (Exception e) { log.debug("Failed to invalidate session, might already be invalidated"); @@ -106,6 +107,7 @@ public class ServletSessionTokenStore implements AdapterTokenStore { final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); HttpSession session = getSession(true); session.setAttribute(KeycloakUndertowAccount.class.getName(), account); + session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); sessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); } diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java index de57268635..e578f85bbe 100755 --- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java +++ b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java @@ -22,6 +22,7 @@ import io.undertow.server.HttpServerExchange; import io.undertow.server.session.Session; import io.undertow.util.Sessions; import org.jboss.logging.Logger; +import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.OidcKeycloakAccount; @@ -101,6 +102,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore { public void saveAccountInfo(OidcKeycloakAccount account) { Session session = Sessions.getOrCreateSession(exchange); session.setAttribute(KeycloakUndertowAccount.class.getName(), account); + session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); sessionManagement.login(session.getSessionManager()); } @@ -111,6 +113,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore { KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); if (account == null) return; session.removeAttribute(KeycloakUndertowAccount.class.getName()); + session.removeAttribute(KeycloakSecurityContext.class.getName()); } @Override diff --git a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java b/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java index 07f2a40f0e..eb17feed55 100755 --- a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java +++ b/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java @@ -18,6 +18,8 @@ package org.keycloak.adapters.saml.jetty; import org.eclipse.jetty.security.DefaultUserIdentity; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.ServerAuthException; import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.security.authentication.DeferredAuthentication; @@ -135,12 +137,46 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator { } + private class DummyLoginService implements LoginService { + @Override + public String getName() { + return null; + } + + @Override + public UserIdentity login(String username, Object credentials) { + return null; + } + + @Override + public boolean validate(UserIdentity user) { + return false; + } + + @Override + public IdentityService getIdentityService() { + return null; + } + + @Override + public void setIdentityService(IdentityService service) { + + } + + @Override + public void logout(UserIdentity user) { + + } + } + @Override public void setConfiguration(AuthConfiguration configuration) { //super.setConfiguration(configuration); initializeKeycloak(); + // need this so that getUserPrincipal does not throw NPE + _loginService = new DummyLoginService(); String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); setErrorPage(error); } diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java index 51bdb4b758..aa75439d11 100755 --- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java +++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java @@ -167,9 +167,9 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i @Override public void invoke(Request request, Response response) throws IOException, ServletException { log.fine("*********************** SAML ************"); + CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request); + SamlDeployment deployment = deploymentContext.resolveDeployment(facade); if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) { - CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request); - SamlDeployment deployment = deploymentContext.resolveDeployment(facade); if (deployment != null && deployment.isConfigured()) { SamlSessionStore tokenStore = getSessionStore(request, facade, deployment); SamlAuthenticator authenticator = new CatalinaSamlEndpoint(facade, deployment, tokenStore); @@ -180,6 +180,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i } try { + getSessionStore(request, facade, deployment).isLoggedIn(); // sets request UserPrincipal if logged in. we do this so that the UserPrincipal is available on unsecured, unconstrainted URLs super.invoke(request, response); } finally { } diff --git a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java index 3806ffeaab..118fd1be73 100755 --- a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java +++ b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java @@ -28,6 +28,9 @@ import java.io.ObjectOutputStream; import java.io.Serializable; /** + * Available in secured requests under HttpServlerRequest.getAttribute() + * Also available in HttpSession.getAttribute under the classname of this class + * * @author Bill Burke * @version $Revision: 1 $ */ diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml b/docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml new file mode 100755 index 0000000000..cfcf18fe87 --- /dev/null +++ b/docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml @@ -0,0 +1,12 @@ + + KeycloakSecurityContext + + The KeycloakSecurityContext interface is available if you need to look at the access token directly. This context is also useful if you need to + get the encoded access token so you can make additional REST invocations. In servlet environments it is available in secured invocations as an attribute in HttpServletRequest. + Or, it is available in secure and insecure requests in the HttpSession for browser apps. + + httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName()); + httpServletRequest.getSession().getAttribute(KeycloakSecurityContext.class.getName()); + + + \ No newline at end of file diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java index f03b55acbb..7b0d9d3891 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java @@ -138,6 +138,12 @@ public class AdapterTestStrategy extends ExternalResource { String pageSource = driver.getPageSource(); System.out.println(pageSource); Assert.assertTrue(pageSource.contains("parameter=hello")); + // test that user principal and KeycloakSecurityContext available + driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal/insecure"); + System.out.println("insecure: "); + System.out.println(driver.getPageSource()); + Assert.assertTrue(driver.getPageSource().contains("Insecure Page")); + if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertTrue(driver.getPageSource().contains("UserPrincipal")); // test logout diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java index 9637617a0e..a53313333b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java @@ -100,6 +100,7 @@ public class FilterAdapterTest { @Test public void testSavedPostRequest() throws Exception { + System.setProperty("insecure.user.principal.unsupported", "true"); testStrategy.testSavedPostRequest(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java index c0135ef454..b6a0bd5b4e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java @@ -17,6 +17,9 @@ package org.keycloak.testsuite.adapter; +import org.junit.Assert; +import org.keycloak.KeycloakSecurityContext; + import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -35,6 +38,17 @@ public class InputServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String appBase = System.getProperty("app.server.base.url", "http://localhost:8081"); + if (req.getRequestURI().endsWith("insecure")) { + if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getUserPrincipal()); + if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getAttribute(KeycloakSecurityContext.class.getName())); + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + pw.printf("%s", "Insecure Page"); + if (req.getUserPrincipal() != null) pw.printf("UserPrincipal: " + req.getUserPrincipal().getName()); + pw.print(""); + pw.flush(); + return; + } String actionUrl = appBase + "/input-portal/secured/post"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java index d47e78f00b..daaf5a7cdd 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java @@ -48,8 +48,8 @@ public class ConcurrencyTest extends AbstractClientTest { private static final Logger log = Logger.getLogger(ConcurrencyTest.class); - private static final int DEFAULT_THREADS = 10; - private static final int DEFAULT_ITERATIONS = 100; + private static final int DEFAULT_THREADS = 5; + private static final int DEFAULT_ITERATIONS = 20; // If enabled only one request is allowed at the time. Useful for checking that test is working. private static final boolean SYNCHRONIZED = false; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java index b6eb54a2de..57c8e48cbf 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java @@ -17,6 +17,9 @@ package org.keycloak.testsuite.keycloaksaml; +import org.junit.Assert; +import org.keycloak.KeycloakSecurityContext; + import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -34,6 +37,16 @@ public class InputServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String appBase = System.getProperty("app.server.base.url", "http://localhost:8081"); + if (req.getRequestURI().endsWith("insecure")) { + if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getUserPrincipal()); + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + pw.printf("%s", "Insecure Page"); + if (req.getUserPrincipal() != null) pw.printf("UserPrincipal: " + req.getUserPrincipal().getName()); + pw.print(""); + pw.flush(); + return; + } String actionUrl = appBase + "/input-portal/secured/post"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java index 948d9cad0a..a9e95bba6b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java @@ -152,6 +152,12 @@ public class SamlAdapterTestStrategy extends ExternalResource { String pageSource = driver.getPageSource(); System.out.println(pageSource); Assert.assertTrue(pageSource.contains("parameter=hello")); + // test that user principal and KeycloakSecurityContext available + driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal/insecure"); + System.out.println("insecure: "); + System.out.println(driver.getPageSource()); + Assert.assertTrue(driver.getPageSource().contains("Insecure Page")); + if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertTrue(driver.getPageSource().contains("UserPrincipal")); // test logout diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index 950fd224ad..e9bb97f419 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -86,7 +86,7 @@ "connectionsInfinispan": { "default": { "clustered": "${keycloak.connectionsInfinispan.clustered:false}", - "async": "${keycloak.connectionsInfinispan.async:true}", + "async": "${keycloak.connectionsInfinispan.async:false}", "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}" } }