Merge pull request #2093 from patriot1burke/master

KEYCLOAK-2377
This commit is contained in:
Bill Burke 2016-01-22 18:20:07 -05:00
commit 4d18812768
76 changed files with 697 additions and 64 deletions

View file

@ -433,6 +433,16 @@ public class AdapterDeploymentContext {
public void setPrincipalAttribute(String principalAttribute) {
delegate.setPrincipalAttribute(principalAttribute);
}
@Override
public boolean isTurnOffChangeSessionIdOnLogin() {
return delegate.isTurnOffChangeSessionIdOnLogin();
}
@Override
public void setTurnOffChangeSessionIdOnLogin(boolean turnOffChangeSessionIdOnLogin) {
delegate.setTurnOffChangeSessionIdOnLogin(turnOffChangeSessionIdOnLogin);
}
}
protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {

View file

@ -57,6 +57,8 @@ public class KeycloakDeployment {
protected boolean alwaysRefreshToken;
protected boolean registerNodeAtStartup;
protected int registerNodePeriod;
protected boolean turnOffChangeSessionIdOnLogin;
protected volatile int notBefore;
public KeycloakDeployment() {
@ -361,4 +363,12 @@ public class KeycloakDeployment {
public void setPrincipalAttribute(String principalAttribute) {
this.principalAttribute = principalAttribute;
}
public boolean isTurnOffChangeSessionIdOnLogin() {
return turnOffChangeSessionIdOnLogin;
}
public void setTurnOffChangeSessionIdOnLogin(boolean turnOffChangeSessionIdOnLogin) {
this.turnOffChangeSessionIdOnLogin = turnOffChangeSessionIdOnLogin;
}
}

View file

@ -88,6 +88,9 @@ public class KeycloakDeploymentBuilder {
throw new RuntimeException("You must specify auth-url");
}
deployment.setAuthServerBaseUrl(adapterConfig);
if (adapterConfig.getTurnOffChangeSessionIdOnLogin() != null) {
deployment.setTurnOffChangeSessionIdOnLogin(adapterConfig.getTurnOffChangeSessionIdOnLogin());
}
log.debug("Use authServerUrl: " + deployment.getAuthServerBaseUrl() + ", tokenUrl: " + deployment.getTokenUrl() + ", relativeUrls: " + deployment.getRelativeUrls());
return deployment;

View file

@ -293,7 +293,7 @@ public class OAuthRequestAuthenticator {
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
try {
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.getHttpSessionId(true) : null;
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
} catch (ServerRequest.HttpFailure failure) {
log.error("failed to turn code into token");

View file

@ -141,7 +141,13 @@ public abstract class RequestAuthenticator {
protected abstract void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
protected abstract void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal, String method);
protected abstract String getHttpSessionId(boolean create);
/**
* After code is received, we change the session id if possible to guard against https://www.owasp.org/index.php/Session_Fixation
* @param create
* @return
*/
protected abstract String changeHttpSessionId(boolean create);
protected void completeAuthentication(BearerTokenRequestAuthenticator bearer, String method) {
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null);

View file

@ -87,6 +87,7 @@
<xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="enable-basic-auth" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>

View file

@ -76,7 +76,7 @@ public class JettyRequestAuthenticator extends RequestAuthenticator {
@Override
protected String getHttpSessionId(boolean create) {
protected String changeHttpSessionId(boolean create) {
HttpSession session = request.getSession(create);
return session != null ? session.getId() : null;
}

View file

@ -0,0 +1,30 @@
package org.keycloak.adapters.jetty;
import org.eclipse.jetty.server.Request;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
import org.keycloak.adapters.spi.HttpFacade;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Jetty91RequestAuthenticator extends JettyRequestAuthenticator {
public Jetty91RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort, Request request) {
super(facade, deployment, tokenStore, sslRedirectPort, request);
}
@Override
protected String changeHttpSessionId(boolean create) {
Request request = this.request;
HttpSession session = request.getSession(false);
if (session == null) {
return request.getSession(true).getId();
}
if (deployment.isTurnOffChangeSessionIdOnLogin() == false) return request.changeSessionId();
else return session.getId();
}
}

View file

@ -7,7 +7,9 @@ import org.eclipse.jetty.server.UserIdentity;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
import javax.servlet.ServletRequest;
@ -42,5 +44,12 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
};
}
@Override
protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade,
KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
return new Jetty91RequestAuthenticator(facade, deployment, tokenStore, -1, request);
}
}

View file

@ -0,0 +1,30 @@
package org.keycloak.adapters.jetty;
import org.eclipse.jetty.server.Request;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
import org.keycloak.adapters.spi.HttpFacade;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Jetty92RequestAuthenticator extends JettyRequestAuthenticator {
public Jetty92RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort, Request request) {
super(facade, deployment, tokenStore, sslRedirectPort, request);
}
@Override
protected String changeHttpSessionId(boolean create) {
Request request = this.request;
HttpSession session = request.getSession(false);
if (session == null) {
return request.getSession(true).getId();
}
if (deployment.isTurnOffChangeSessionIdOnLogin() == false) return request.changeSessionId();
else return session.getId();
}
}

View file

@ -7,7 +7,9 @@ import org.eclipse.jetty.server.UserIdentity;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
import javax.servlet.ServletRequest;
@ -41,4 +43,11 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
public AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
}
@Override
protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade,
KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
return new Jetty92RequestAuthenticator(facade, deployment, tokenStore, -1, request);
}
}

View file

@ -14,7 +14,6 @@
<packaging>pom</packaging>
<modules>
<module>jetty-adapter-spi</module>
<module>jetty-core</module>
<module>jetty8.1</module>
<module>jetty9.2</module>

View file

@ -78,7 +78,7 @@ public class FilterRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected String getHttpSessionId(boolean create) {
protected String changeHttpSessionId(boolean create) {
HttpSession session = request.getSession(create);
return session != null ? session.getId() : null;
}

View file

@ -82,7 +82,7 @@ public class SpringSecurityRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected String getHttpSessionId(boolean create) {
protected String changeHttpSessionId(boolean create) {
HttpSession session = request.getSession(create);
return session != null ? session.getId() : null;
}

View file

@ -95,13 +95,13 @@ public class SpringSecurityRequestAuthenticatorTest {
@Test
public void testGetHttpSessionIdTrue() throws Exception {
String sessionId = authenticator.getHttpSessionId(true);
String sessionId = authenticator.changeHttpSessionId(true);
assertNotNull(sessionId);
}
@Test
public void testGetHttpSessionIdFalse() throws Exception {
String sessionId = authenticator.getHttpSessionId(false);
String sessionId = authenticator.changeHttpSessionId(false);
assertNull(sessionId);
}
}

View file

@ -185,7 +185,7 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
nodesRegistrationManagement.tryRegister(deployment);
CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, tokenStore, facade, request, createPrincipalFactory());
CatalinaRequestAuthenticator authenticator = createRequestAuthenticator(request, facade, deployment, tokenStore);
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
if (facade.isEnded()) {
@ -200,6 +200,10 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
return false;
}
protected CatalinaRequestAuthenticator createRequestAuthenticator(Request request, CatalinaHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
return new CatalinaRequestAuthenticator(deployment, tokenStore, facade, request, createPrincipalFactory());
}
/**
* Checks that access token is still valid. Will attempt refresh of token if it is not.
*
@ -230,7 +234,7 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
}
if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) {
store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement, createPrincipalFactory(), this);
store = createSessionTokenStore(request, resolvedDeployment);
} else {
store = new CatalinaCookieTokenStore(request, facade, resolvedDeployment, createPrincipalFactory());
}
@ -239,4 +243,10 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
return store;
}
private AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
AdapterTokenStore store;
store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement, createPrincipalFactory(), this);
return store;
}
}

View file

@ -83,7 +83,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected String getHttpSessionId(boolean create) {
protected String changeHttpSessionId(boolean create) {
HttpSession session = request.getSession(create);
return session != null ? session.getId() : null;
}

View file

@ -6,6 +6,8 @@ import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.realm.GenericPrincipal;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
@ -55,4 +57,5 @@ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorVal
}
};
}
}

View file

@ -5,6 +5,8 @@ import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.realm.GenericPrincipal;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
@ -51,4 +53,5 @@ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorVal
}
};
}
}

View file

@ -7,6 +7,9 @@ import org.apache.catalina.core.StandardContext;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.spi.HttpFacade;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
@ -71,4 +74,9 @@ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorVal
}
};
}
@Override
protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) {
return super.getTokenStore(request, facade, resolvedDeployment);
}
}

View file

@ -0,0 +1,28 @@
package org.keycloak.adapters.tomcat;
import org.apache.catalina.connector.Request;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Tomcat8RequestAuthenticator extends CatalinaRequestAuthenticator {
public Tomcat8RequestAuthenticator(KeycloakDeployment deployment, AdapterTokenStore tokenStore, CatalinaHttpFacade facade, Request request, GenericPrincipalFactory principalFactory) {
super(deployment, tokenStore, facade, request, principalFactory);
}
@Override
protected String changeHttpSessionId(boolean create) {
Request request = this.request;
HttpSession session = request.getSession(false);
if (session == null) {
return request.getSession(true).getId();
}
if (deployment.isTurnOffChangeSessionIdOnLogin() == false) return request.changeSessionId();
else return session.getId();
}
}

View file

@ -72,7 +72,7 @@ public abstract class AbstractUndertowRequestAuthenticator extends RequestAuthen
}
@Override
protected String getHttpSessionId(boolean create) {
protected String changeHttpSessionId(boolean create) {
if (create) {
Session session = Sessions.getOrCreateSession(exchange);
return session.getId();

View file

@ -177,7 +177,7 @@ public class KeycloakServletExtension implements ServletExtension {
ServletSessionConfig cookieConfig = new ServletSessionConfig();
cookieConfig.setPath(deploymentInfo.getContextPath());
deploymentInfo.setServletSessionConfig(cookieConfig);
ChangeSessionIdOnLogin.turnOffChangeSessionIdOnLogin(deploymentInfo);
ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo);
deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationManagementWrapper.class, new InstanceFactory<UndertowNodesRegistrationManagementWrapper>() {
@Override

View file

@ -63,6 +63,11 @@ public class ServletRequestAuthenticator extends AbstractUndertowRequestAuthenti
}
@Override
protected String changeHttpSessionId(boolean create) {
if (deployment.isTurnOffChangeSessionIdOnLogin() == false) return ChangeSessionId.changeSessionId(exchange, create);
else return getHttpSessionId(create);
}
protected String getHttpSessionId(boolean create) {
HttpSession session = getSession(create);
return session != null ? session.getId() : null;

View file

@ -77,6 +77,12 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition {
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final SimpleAttributeDefinition TURN_OFF_CHANGE_SESSION =
new SimpleAttributeDefinitionBuilder("turn-off-change-session-id-on-login", ModelType.BOOLEAN, true)
.setXmlName("turn-off-change-session-id-on-login")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
static {
@ -86,6 +92,7 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition {
DEPLOYMENT_ONLY_ATTRIBUTES.add(BEARER_ONLY);
DEPLOYMENT_ONLY_ATTRIBUTES.add(ENABLE_BASIC_AUTH);
DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_CLIENT);
DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION);
}
protected static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();

View file

@ -63,6 +63,7 @@ keycloak.secure-deployment.register-node-at-startup=Cluster setting
keycloak.secure-deployment.register-node-period=how often to re-register node
keycloak.secure-deployment.token-store=cookie or session storage for auth session data
keycloak.secure-deployment.principal-attribute=token attribute to use to set Principal name
keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak.secure-deployment.credential=Credential value

View file

@ -87,6 +87,7 @@
<xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="enable-basic-auth" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>

View file

@ -3,6 +3,7 @@
<realm>master</realm>
<resource>web-console</resource>
<use-resource-role-mappings>true</use-resource-role-mappings>
<turn-off-change-session-id-on-login>false</turn-off-change-session-id-on-login>
<realm-public-key>
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB
</realm-public-key>

View file

@ -77,6 +77,12 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition {
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final SimpleAttributeDefinition TURN_OFF_CHANGE_SESSION =
new SimpleAttributeDefinitionBuilder("turn-off-change-session-id-on-login", ModelType.BOOLEAN, true)
.setXmlName("turn-off-change-session-id-on-login")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final List<SimpleAttributeDefinition> DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
static {
@ -86,6 +92,7 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition {
DEPLOYMENT_ONLY_ATTRIBUTES.add(BEARER_ONLY);
DEPLOYMENT_ONLY_ATTRIBUTES.add(ENABLE_BASIC_AUTH);
DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_CLIENT);
DEPLOYMENT_ONLY_ATTRIBUTES.add(TURN_OFF_CHANGE_SESSION);
}
protected static final List<SimpleAttributeDefinition> ALL_ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();

View file

@ -63,6 +63,7 @@ keycloak.secure-deployment.register-node-at-startup=Cluster setting
keycloak.secure-deployment.register-node-period=how often to re-register node
keycloak.secure-deployment.token-store=cookie or session storage for auth session data
keycloak.secure-deployment.principal-attribute=token attribute to use to set Principal name
keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak.secure-deployment.credential=Credential value

View file

@ -87,6 +87,7 @@
<xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="enable-basic-auth" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>

View file

@ -3,6 +3,7 @@
<realm>master</realm>
<resource>web-console</resource>
<use-resource-role-mappings>true</use-resource-role-mappings>
<turn-off-change-session-id-on-login>false</turn-off-change-session-id-on-login>
<realm-public-key>
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4siLKUew0WYxdtq6/rwk4Uj/4amGFFnE/yzIxQVU0PUqz3QBRVkUWpDj0K6ZnS5nzJV/y6DHLEy7hjZTdRDphyF1sq09aDOYnVpzu8o2sIlMM8q5RnUyEfIyUZqwo8pSZDJ90fS0s+IDUJNCSIrAKO3w1lqZDHL6E/YFHXyzkvQIDAQAB
</realm-public-key>

View file

@ -201,6 +201,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
private String nameIDPolicyFormat;
private boolean forceAuthentication;
private boolean isPassive;
private boolean turnOffChangeSessionIdOnLogin;
private PrivateKey decryptionKey;
private KeyPair signingKeyPair;
private String assertionConsumerServiceUrl;
@ -211,6 +212,16 @@ public class DefaultSamlDeployment implements SamlDeployment {
private SignatureAlgorithm signatureAlgorithm;
private String signatureCanonicalizationMethod;
@Override
public boolean turnOffChangeSessionIdOnLogin() {
return turnOffChangeSessionIdOnLogin;
}
public void setTurnOffChangeSessionIdOnLogin(boolean turnOffChangeSessionIdOnLogin) {
this.turnOffChangeSessionIdOnLogin = turnOffChangeSessionIdOnLogin;
}
@Override
public IDP getIDP() {
return idp;

View file

@ -57,6 +57,7 @@ public interface SamlDeployment {
String getNameIDPolicyFormat();
boolean isForceAuthentication();
boolean isIsPassive();
boolean turnOffChangeSessionIdOnLogin();
PrivateKey getDecryptionKey();
KeyPair getSigningKeyPair();
String getSignatureCanonicalizationMethod();

View file

@ -34,6 +34,7 @@ public class SP implements Serializable {
private String sslPolicy;
private boolean forceAuthentication;
private boolean isPassive;
private boolean turnOffChangeSessionIdOnLogin;
private String logoutPage;
private List<Key> keys;
private String nameIDPolicyFormat;
@ -73,6 +74,14 @@ public class SP implements Serializable {
this.isPassive = isPassive;
}
public boolean isTurnOffChangeSessionIdOnLogin() {
return turnOffChangeSessionIdOnLogin;
}
public void setTurnOffChangeSessionIdOnLogin(boolean turnOffChangeSessionIdOnLogin) {
this.turnOffChangeSessionIdOnLogin = turnOffChangeSessionIdOnLogin;
}
public List<Key> getKeys() {
return keys;
}

View file

@ -12,6 +12,7 @@ public class ConfigXmlConstants {
public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat";
public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication";
public static final String IS_PASSIVE_ATTR = "isPassive";
public static final String TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN_ATTR = "turnOffChangeSessionIdOnLogin";
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
public static final String LOGOUT_PAGE_ATTR = "logoutPage";

View file

@ -66,6 +66,7 @@ public class SPXmlParser extends AbstractParser {
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
sp.setTurnOffChangeSessionIdOnLogin(getBooleanAttributeValue(startElement, ConfigXmlConstants.TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN_ATTR));
while (xmlEventReader.hasNext()) {
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
if (xmlEvent == null)

View file

@ -34,6 +34,7 @@
<xs:attribute name="logoutPage" type="xs:string" use="optional"/>
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional"/>
<xs:attribute name="isPassive" type="xs:boolean" use="optional"/>
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional"/>
</xs:complexType>
<xs:complexType name="keys-type">

View file

@ -75,12 +75,18 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
if (store != null) {
return store;
}
store = new JettySamlSessionStore(request, createSessionTokenStore(request, resolvedDeployment), facade, idMapper, new JettyUserSessionManagement(request.getSessionManager()));
store = createJettySamlSessionStore(request, facade, resolvedDeployment);
request.setAttribute(TOKEN_STORE_NOTE, store);
return store;
}
protected JettySamlSessionStore createJettySamlSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
JettySamlSessionStore store;
store = new JettySamlSessionStore(request, createSessionTokenStore(request, resolvedDeployment), facade, idMapper, new JettyUserSessionManagement(request.getSessionManager()), resolvedDeployment);
return store;
}
public abstract AdapterSessionStore createSessionTokenStore(Request request, SamlDeployment resolvedDeployment);
public void logoutCurrent(Request request) {

View file

@ -2,6 +2,7 @@ package org.keycloak.adapters.saml.jetty;
import org.eclipse.jetty.server.Request;
import org.jboss.logging.Logger;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.AdapterSessionStore;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.SessionIdMapper;
@ -23,19 +24,21 @@ import java.util.Set;
public class JettySamlSessionStore implements SamlSessionStore {
public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI";
private static final Logger log = Logger.getLogger(JettySamlSessionStore.class);
private Request request;
protected Request request;
protected AdapterSessionStore sessionStore;
protected HttpFacade facade;
protected SessionIdMapper idMapper;
protected JettyUserSessionManagement sessionManagement;
protected final SamlDeployment deployment;
public JettySamlSessionStore(Request request, AdapterSessionStore sessionStore, HttpFacade facade,
SessionIdMapper idMapper, JettyUserSessionManagement sessionManagement) {
SessionIdMapper idMapper, JettyUserSessionManagement sessionManagement, SamlDeployment deployment) {
this.request = request;
this.sessionStore = sessionStore;
this.facade = facade;
this.idMapper = idMapper;
this.sessionManagement = sessionManagement;
this.deployment = deployment;
}
@Override
@ -132,10 +135,14 @@ public class JettySamlSessionStore implements SamlSessionStore {
HttpSession session = request.getSession(true);
session.setAttribute(SamlSession.class.getName(), account);
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), session.getId());
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), changeSessionId(session));
}
protected String changeSessionId(HttpSession session) {
return session.getId();
}
@Override
public SamlSession getAccount() {
HttpSession session = request.getSession(true);

View file

@ -0,0 +1,27 @@
package org.keycloak.adapters.saml.jetty;
import org.eclipse.jetty.server.Request;
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.AdapterSessionStore;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.SessionIdMapper;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Jetty9SamlSessionStore extends JettySamlSessionStore {
public Jetty9SamlSessionStore(Request request, AdapterSessionStore sessionStore, HttpFacade facade, SessionIdMapper idMapper, JettyUserSessionManagement sessionManagement, SamlDeployment deployment) {
super(request, sessionStore, facade, idMapper, sessionManagement, deployment);
}
@Override
protected String changeSessionId(HttpSession session) {
Request request = this.request;
if (deployment.turnOffChangeSessionIdOnLogin() == false) return request.changeSessionId();
else return session.getId();
}
}

View file

@ -4,8 +4,10 @@ import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity;
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
import org.keycloak.adapters.spi.AdapterSessionStore;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.HttpFacade;
import javax.servlet.ServletRequest;
@ -40,5 +42,13 @@ public class KeycloakSamlAuthenticator extends AbstractSamlAuthenticator {
};
}
@Override
protected JettySamlSessionStore createJettySamlSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
JettySamlSessionStore store;
store = new Jetty9SamlSessionStore(request, createSessionTokenStore(request, resolvedDeployment), facade, idMapper, new JettyUserSessionManagement(request.getSessionManager()), resolvedDeployment);
return store;
}
}

View file

@ -0,0 +1,27 @@
package org.keycloak.adapters.saml.jetty;
import org.eclipse.jetty.server.Request;
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.AdapterSessionStore;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.SessionIdMapper;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Jetty9SamlSessionStore extends JettySamlSessionStore {
public Jetty9SamlSessionStore(Request request, AdapterSessionStore sessionStore, HttpFacade facade, SessionIdMapper idMapper, JettyUserSessionManagement sessionManagement, SamlDeployment deployment) {
super(request, sessionStore, facade, idMapper, sessionManagement, deployment);
}
@Override
protected String changeSessionId(HttpSession session) {
Request request = this.request;
if (deployment.turnOffChangeSessionIdOnLogin() == false) return request.changeSessionId();
else return session.getId();
}
}

View file

@ -4,8 +4,10 @@ import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity;
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
import org.keycloak.adapters.spi.AdapterSessionStore;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.HttpFacade;
import javax.servlet.ServletRequest;
@ -39,4 +41,11 @@ public class KeycloakSamlAuthenticator extends AbstractSamlAuthenticator {
public AdapterSessionStore createSessionTokenStore(Request request, SamlDeployment resolvedDeployment) {
return new JettyAdapterSessionStore(request);
}
@Override
protected JettySamlSessionStore createJettySamlSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
JettySamlSessionStore store;
store = new Jetty9SamlSessionStore(request, createSessionTokenStore(request, resolvedDeployment), facade, idMapper, new JettyUserSessionManagement(request.getSessionManager()), resolvedDeployment);
return store;
}
}

View file

@ -61,7 +61,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
protected void logoutInternal(Request request) {
CatalinaHttpFacade facade = new CatalinaHttpFacade(null, request);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
SamlSessionStore tokenStore = getTokenStore(request, facade, deployment);
SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
tokenStore.logoutAccount();
request.setUserPrincipal(null);
}
@ -184,7 +184,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
log.fine("deployment not configured");
return false;
}
SamlSessionStore tokenStore = getTokenStore(request, facade, deployment);
SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
CatalinaSamlAuthenticator authenticator = new CatalinaSamlAuthenticator(facade, deployment, tokenStore);
@ -229,16 +229,22 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
}
}
protected SamlSessionStore getTokenStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
protected SamlSessionStore getSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
SamlSessionStore store = (SamlSessionStore)request.getNote(TOKEN_STORE_NOTE);
if (store != null) {
return store;
}
store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, request, this, facade);
store = createSessionStore(request, facade, resolvedDeployment);
request.setNote(TOKEN_STORE_NOTE, store);
return store;
}
protected SamlSessionStore createSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
SamlSessionStore store;
store = new CatalinaSamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, request, this, facade, resolvedDeployment);
return store;
}
}

View file

@ -32,15 +32,18 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
protected final Request request;
protected final AbstractSamlAuthenticatorValve valve;
protected final HttpFacade facade;
protected final SamlDeployment deployment;
public CatalinaSamlSessionStore(CatalinaUserSessionManagement sessionManagement, GenericPrincipalFactory principalFactory,
SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade) {
SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade,
SamlDeployment deployment) {
this.sessionManagement = sessionManagement;
this.principalFactory = principalFactory;
this.idMapper = idMapper;
this.request = request;
this.valve = valve;
this.facade = facade;
this.deployment = deployment;
}
@Override
@ -173,10 +176,15 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
}
request.setUserPrincipal(principal);
request.setAuthType("KEYCLOAK-SAML");
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), session.getId());
String newId = changeSessionId(session);
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), newId);
}
protected String changeSessionId(Session session) {
return session.getId();
}
@Override
public SamlSession getAccount() {
HttpSession session = getSession(true);

View file

@ -6,6 +6,10 @@ import org.apache.catalina.core.StandardContext;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.keycloak.adapters.saml.AbstractSamlAuthenticatorValve;
import org.keycloak.adapters.saml.CatalinaSamlSessionStore;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
import javax.servlet.http.HttpServletResponse;
@ -68,4 +72,12 @@ public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve {
}
};
}
@Override
protected SamlSessionStore createSessionStore(Request request, HttpFacade facade, SamlDeployment resolvedDeployment) {
SamlSessionStore store;
store = new Tomcat8SamlSessionStore(userSessionManagement, createPrincipalFactory(), mapper, request, this, facade, resolvedDeployment);
return store;
}
}

View file

@ -0,0 +1,28 @@
package org.keycloak.adapters.saml.tomcat;
import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.keycloak.adapters.saml.AbstractSamlAuthenticatorValve;
import org.keycloak.adapters.saml.CatalinaSamlSessionStore;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement;
import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Tomcat8SamlSessionStore extends CatalinaSamlSessionStore {
public Tomcat8SamlSessionStore(CatalinaUserSessionManagement sessionManagement, GenericPrincipalFactory principalFactory, SessionIdMapper idMapper, Request request, AbstractSamlAuthenticatorValve valve, HttpFacade facade, SamlDeployment deployment) {
super(sessionManagement, principalFactory, idMapper, request, valve, facade, deployment);
}
@Override
protected String changeSessionId(Session session) {
Request request = this.request;
if (deployment.turnOffChangeSessionIdOnLogin() == false) return request.changeSessionId();
else return session.getId();
}
}

View file

@ -35,7 +35,7 @@ import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
import org.keycloak.adapters.undertow.ChangeSessionIdOnLogin;
import org.keycloak.adapters.undertow.ChangeSessionId;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import org.keycloak.saml.common.exceptions.ParsingException;
@ -184,7 +184,7 @@ public class SamlServletExtension implements ServletExtension {
ServletSessionConfig cookieConfig = new ServletSessionConfig();
cookieConfig.setPath(deploymentInfo.getContextPath());
deploymentInfo.setServletSessionConfig(cookieConfig);
ChangeSessionIdOnLogin.turnOffChangeSessionIdOnLogin(deploymentInfo);
ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo);
}

View file

@ -32,7 +32,7 @@ public class ServletSamlAuthMech extends AbstractSamlAuthMech {
@Override
protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) {
return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper);
return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment);
}
@Override

View file

@ -7,9 +7,11 @@ import io.undertow.server.session.SessionManager;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.servlet.spec.HttpSessionImpl;
import org.jboss.logging.Logger;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.undertow.ChangeSessionId;
import org.keycloak.adapters.undertow.SavedRequest;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import org.keycloak.common.util.KeycloakUriBuilder;
@ -36,15 +38,17 @@ public class ServletSamlSessionStore implements SamlSessionStore {
private final UndertowUserSessionManagement sessionManagement;
private final SecurityContext securityContext;
private final SessionIdMapper idMapper;
protected final SamlDeployment deployment;
public ServletSamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
SecurityContext securityContext,
SessionIdMapper idMapper) {
SessionIdMapper idMapper, SamlDeployment deployment) {
this.exchange = exchange;
this.sessionManagement = sessionManagement;
this.securityContext = securityContext;
this.idMapper = idMapper;
this.deployment = deployment;
}
@Override
@ -155,10 +159,16 @@ public class ServletSamlSessionStore implements SamlSessionStore {
HttpSession session = getSession(true);
session.setAttribute(SamlSession.class.getName(), account);
sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), session.getId());
String sessionId = changeSessionId(session);
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
}
protected String changeSessionId(HttpSession session) {
if (deployment.turnOffChangeSessionIdOnLogin() == false) return ChangeSessionId.changeSessionId(exchange, false);
else return session.getId();
}
@Override
public SamlSession getAccount() {
HttpSession session = getSession(true);

View file

@ -20,6 +20,6 @@ public class WildflySamlAuthMech extends ServletSamlAuthMech {
@Override
protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) {
return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper);
return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment);
}
}

View file

@ -2,6 +2,7 @@ package org.keycloak.adapters.saml.wildfly;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.undertow.ServletSamlSessionStore;
@ -13,8 +14,8 @@ import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
*/
public class WildflySamlSessionStore extends ServletSamlSessionStore {
public WildflySamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
SecurityContext securityContext, SessionIdMapper idMapper) {
super(exchange, sessionManagement, securityContext, idMapper);
SecurityContext securityContext, SessionIdMapper idMapper, SamlDeployment resolvedDeployment) {
super(exchange, sessionManagement, securityContext, idMapper, resolvedDeployment);
}
@Override

View file

@ -30,6 +30,8 @@ public class Constants {
static final String NAME_ID_POLICY_FORMAT = "name-id-policy-format";
static final String LOGOUT_PAGE = "logout-page";
static final String FORCE_AUTHENTICATION = "force-authentication";
static final String IS_PASSIVE = "isPassive";
static final String TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN = "turnOffChangeSessionIdOnLogin";
static final String ROLE_ATTRIBUTES = "role-attributes";
static final String SIGNING = "signing";
static final String ENCRYPTION = "encryption";
@ -87,6 +89,8 @@ public class Constants {
static final String KEY_STORE = "KeyStore";
static final String PRIVATE_KEY = "PrivateKey";
static final String CERTIFICATE = "Certificate";
static final String IS_PASSIVE = "isPassive";
static final String TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN = "turnOffChangeSessionIdOnLogin";
static final String PRIVATE_KEY_ALIAS = "alias";
static final String PRIVATE_KEY_PASSWORD = "password";

View file

@ -59,6 +59,15 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.setXmlName(Constants.XML.FORCE_AUTHENTICATION)
.build();
static final SimpleAttributeDefinition IS_PASSIVE =
new SimpleAttributeDefinitionBuilder(Constants.Model.IS_PASSIVE, ModelType.BOOLEAN, true)
.setXmlName(Constants.XML.IS_PASSIVE)
.build();
static final SimpleAttributeDefinition TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN =
new SimpleAttributeDefinitionBuilder(Constants.Model.TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN, ModelType.BOOLEAN, true)
.setXmlName(Constants.XML.TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN)
.build();
static final SimpleAttributeDefinition PRINCIPAL_NAME_MAPPING_POLICY =
new SimpleAttributeDefinitionBuilder(Constants.Model.PRINCIPAL_NAME_MAPPING_POLICY, ModelType.STRING, true)
.setXmlName(Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY)
@ -74,7 +83,7 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.setAllowNull(false)
.build();
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION};
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION, IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN};
static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES};

View file

@ -15,6 +15,8 @@ keycloak-saml.service-provider.ssl-policy=SSL Policy to use
keycloak-saml.service-provider.name-id-policy-format=Name ID policy format URN
keycloak-saml.service-provider.logout-page=URI to a logout page
keycloak-saml.service-provider.force-authentication=Redirected unauthenticated request to a login page
keycloak-saml.service-provider.isPassive=If user isn't logged in just return with an error. Used to check if a user is already logged in or not
keycloak-saml.service-provider.turnOffChangeSessionIdOnLogin=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak-saml.service-provider.role-attributes=Role identifiers
keycloak-saml.service-provider.principal-name-mapping-policy=Principal name mapping policy
keycloak-saml.service-provider.principal-name-mapping-attribute-name=Principal name mapping attribute name

View file

@ -66,6 +66,16 @@
<xs:documentation>Redirected unauthenticated request to a login page</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="isPassive" type="xs:boolean" use="required">
<xs:annotation>
<xs:documentation>If user isn't logged in just return with an error. Used to check if a user is already logged in or not</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="required">
<xs:annotation>
<xs:documentation>The session id is changed by default on a successful login. Change this to true if you want to turn this off</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="identity-provider-type">
<xs:all minOccurs="1" maxOccurs="1">

View file

@ -4,7 +4,9 @@
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false">
forceAuthentication="false"
isPassive="true"
turnOffChangeSessionIdOnLogin="true">
<Keys>
<Key encryption="true" signing="true">

View file

@ -5,7 +5,7 @@
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.9.0.CR1-SNAPSHOT</version>
<relativePath>../../../../pom.xml</relativePath>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View file

@ -19,5 +19,6 @@
<module>undertow-adapter-spi</module>
<module>servlet-adapter-spi</module>
<module>jboss-adapter-core</module>
<module>jetty-adapter-spi</module>
</modules>
</project>

View file

@ -0,0 +1,53 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.server.session.SessionConfig;
import io.undertow.server.session.SessionManager;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.servlet.spec.HttpSessionImpl;
import io.undertow.servlet.spec.ServletContextImpl;
import java.lang.reflect.Method;
import java.security.AccessController;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ChangeSessionId {
/**
* This is a hack to be backward compatible between Undertow 1.3+ and versions lower. In Undertow 1.3, a new
* switch was added setChangeSessionIdOnLogin, this screws up session management for keycloak as after the session id
* is uploaded to Keycloak, undertow changes the session id and it can't be invalidated.
*
* @param deploymentInfo
*/
public static void turnOffChangeSessionIdOnLogin(DeploymentInfo deploymentInfo) {
try {
Method method = DeploymentInfo.class.getMethod("setChangeSessionIdOnLogin", boolean.class);
method.invoke(deploymentInfo, false);
} catch (Exception ignore) {
}
}
public static String changeSessionId(HttpServerExchange exchange, boolean create) {
final ServletRequestContext sc = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletContextImpl currentServletContext = sc.getCurrentServletContext();
HttpSessionImpl session = currentServletContext.getSession(exchange, create);
if (session == null) {
return null;
}
Session underlyingSession;
if(System.getSecurityManager() == null) {
underlyingSession = session.getSession();
} else {
underlyingSession = AccessController.doPrivileged(new HttpSessionImpl.UnwrapSessionAction(session));
}
return underlyingSession.changeSessionId(exchange, currentServletContext.getSessionConfig());
}
}

View file

@ -1,27 +0,0 @@
package org.keycloak.adapters.undertow;
import io.undertow.servlet.api.DeploymentInfo;
import java.lang.reflect.Method;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ChangeSessionIdOnLogin {
/**
* This is a hack to be backward compatible between Undertow 1.3+ and versions lower. In Undertow 1.3, a new
* switch was added setChangeSessionIdOnLogin, this screws up session management for keycloak as after the session id
* is uploaded to Keycloak, undertow changes the session id and it can't be invalidated.
*
* @param deploymentInfo
*/
public static void turnOffChangeSessionIdOnLogin(DeploymentInfo deploymentInfo) {
try {
Method method = DeploymentInfo.class.getMethod("setChangeSessionIdOnLogin", boolean.class);
method.invoke(deploymentInfo, false);
} catch (Exception ignore) {
}
}
}

View file

@ -50,6 +50,8 @@ public class AdapterConfig extends BaseAdapterConfig {
protected String tokenStore;
@JsonProperty("principal-attribute")
protected String principalAttribute;
@JsonProperty("turn-off-change-session-id-on-login")
protected Boolean turnOffChangeSessionIdOnLogin;
public boolean isAllowAnyHostname() {
return allowAnyHostname;
@ -162,4 +164,12 @@ public class AdapterConfig extends BaseAdapterConfig {
public void setPrincipalAttribute(String principalAttribute) {
this.principalAttribute = principalAttribute;
}
public Boolean getTurnOffChangeSessionIdOnLogin() {
return turnOffChangeSessionIdOnLogin;
}
public void setTurnOffChangeSessionIdOnLogin(Boolean turnOffChangeSessionIdOnLogin) {
this.turnOffChangeSessionIdOnLogin = turnOffChangeSessionIdOnLogin;
}
}

View file

@ -104,6 +104,14 @@
with mongo and infinispan under modules keycloak-model-mongo and keycloak-model-infinispan.
</para>
</simplesect>
<simplesect>
<title>For adapters, session id changed after login</title>
<para>
To plug a security attack vector, for platforms that support it (Tomcat 8, Undertow/Wildfly, Jetty 9),
the keycloak oidc and saml adapters will change the session id after login. You can turn off this behavior
check adapter config switches.
</para>
</simplesect>
</section>
<section>
<title>Migrating to 1.8.0</title>

View file

@ -379,6 +379,15 @@
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>turn-off-change-session-id-on-login</term>
<listitem>
<para>
The session id is changed by default on a successful login on some platforms to plug a security attack vector (Tomcat 8, Jetty9, Undertow/Wildfly). Change this to true if you want to turn this off
This is <emphasis>OPTIONAL</emphasis>. The default value is <emphasis>false</emphasis>.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>

View file

@ -12,7 +12,8 @@
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false"
isPassive="false">
isPassive="false"
turnOffChangeSessionIdOnLogin="false">
<Keys>
<Key signing="true" >
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
@ -125,6 +126,15 @@
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>turnOffChangeSessionIdOnLogin</term>
<listitem>
<para>
The session id is changed by default on a successful login on some platforms to plug a security attack vector (Tomcat 8, Jetty9, Undertow/Wildfly). Change this to true if you want to turn this off
This is <emphasis>OPTIONAL</emphasis>. The default value is <emphasis>false</emphasis>.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>

View file

@ -0,0 +1,35 @@
package org.keycloak.testsuite.keycloaksaml;
import org.keycloak.testsuite.pages.AbstractPage;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class InputPage extends AbstractPage {
@FindBy(id = "parameter")
private WebElement parameter;
@FindBy(name = "submit")
private WebElement submit;
public void execute(String param) {
parameter.clear();
parameter.sendKeys(param);
submit.click();
}
public boolean isCurrent() {
return driver.getTitle().equals("Input Page");
}
@Override
public void open() {
}
}

View file

@ -0,0 +1,52 @@
package org.keycloak.testsuite.keycloaksaml;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
public class InputServlet extends HttpServlet {
private static final String FORM_URLENCODED = "application/x-www-form-urlencoded";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String appBase = System.getProperty("app.server.base.url", "http://localhost:8081");
String actionUrl = appBase + "/input-portal/secured/post";
resp.setContentType("text/html");
PrintWriter pw = resp.getWriter();
pw.printf("<html><head><title>%s</title></head><body>", "Input Page");
pw.printf("<form action=\"%s\" method=\"POST\">", actionUrl);
pw.println("<input id=\"parameter\" type=\"text\" name=\"parameter\">");
pw.println("<input name=\"submit\" type=\"submit\" value=\"Submit\"></form>");
pw.print("</body></html>");
pw.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (!FORM_URLENCODED.equals(req.getContentType())) {
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
PrintWriter pw = resp.getWriter();
resp.setContentType("text/plain");
pw.printf("Expecting content type " + FORM_URLENCODED +
", received " + req.getContentType() + " instead");
pw.flush();
return;
}
resp.setContentType("text/plain");
PrintWriter pw = resp.getWriter();
pw.printf("parameter="+req.getParameter("parameter"));
pw.flush();
}
}

View file

@ -31,6 +31,8 @@ public class SamlAdapterTest {
initializeSamlSecuredWar("/keycloak-saml/bad-client-signed-post", "/bad-client-sales-post-sig", "bad-client-post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
System.setProperty("app.server.base.url", "http://localhost:8081");
initializeSamlSecuredWar("/keycloak-saml/simple-input", "/input-portal", "input.war", classLoader, InputServlet.class, "/secured/*");
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
server.getServer().deploy(createDeploymentInfo("employee.war", "/employee", SamlSPFacade.class));
@ -63,6 +65,11 @@ public class SamlAdapterTest {
});
}
//@Test Doesn't work for Wildfly as the input stream is read by getParameter for SAML POST binding
public void testSavedPostRequest() throws Exception {
testStrategy.testSavedPostRequest();
}
@Test
public void testErrorHandling() throws Exception {
testStrategy.testErrorHandling();

View file

@ -3,6 +3,7 @@ package org.keycloak.testsuite.keycloaksaml;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.rules.ExternalResource;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.saml.SamlAuthenticationError;
import org.keycloak.adapters.saml.SamlPrincipal;
import org.keycloak.admin.client.Keycloak;
@ -12,6 +13,7 @@ import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.GroupMembershipMapper;
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
@ -27,6 +29,7 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.adapter.*;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.rule.ErrorServlet;
@ -38,7 +41,10 @@ import org.w3c.dom.Document;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;
import java.util.HashSet;
@ -70,6 +76,8 @@ public class SamlAdapterTestStrategy extends ExternalResource {
protected WebDriver driver;
@WebResource
protected LoginPage loginPage;
@WebResource
protected InputPage inputPage;
@Override
protected void before() throws Throwable {
@ -101,6 +109,38 @@ public class SamlAdapterTestStrategy extends ExternalResource {
Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
}
public void testSavedPostRequest() throws Exception {
// test login to customer-portal which does a bearer request to customer-db
driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal");
System.out.println("Current url: " + driver.getCurrentUrl());
Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/input-portal"));
inputPage.execute("hello");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke@redhat.com", "password");
System.out.println("Current url: " + driver.getCurrentUrl());
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/input-portal/secured/post");
String pageSource = driver.getPageSource();
System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("parameter=hello"));
// test logout
driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal?GLO=true");
// test unsecured POST KEYCLOAK-901
Client client = ClientBuilder.newClient();
Form form = new Form();
form.param("parameter", "hello");
String text = client.target(APP_SERVER_BASE_URL + "/input-portal/unsecured").request().post(Entity.form(form), String.class);
Assert.assertTrue(text.contains("parameter=hello"));
client.close();
}
public void testErrorHandling() throws Exception {
ErrorServlet.authError = null;
Client client = ClientBuilder.newClient();

View file

@ -15,6 +15,7 @@ import io.undertow.servlet.api.WebResourceCollection;
import org.keycloak.adapters.saml.undertow.SamlServletExtension;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import javax.servlet.Servlet;
import java.io.IOException;
import java.net.URL;
@ -92,12 +93,19 @@ public abstract class SamlKeycloakRule extends AbstractKeycloakRule {
public void initializeSamlSecuredWar(String warResourcePath, String contextPath, String warDeploymentName, ClassLoader classLoader) {
ServletInfo regularServletInfo = new ServletInfo("servlet", SendUsernameServlet.class)
Class<SendUsernameServlet> servletClass = SendUsernameServlet.class;
String constraintUrl = "/*";
initializeSamlSecuredWar(warResourcePath, contextPath, warDeploymentName, classLoader, servletClass, constraintUrl);
}
public void initializeSamlSecuredWar(String warResourcePath, String contextPath, String warDeploymentName, ClassLoader classLoader, Class<? extends Servlet> servletClass, String constraintUrl) {
ServletInfo regularServletInfo = new ServletInfo("servlet", servletClass)
.addMapping("/*");
SecurityConstraint constraint = new SecurityConstraint();
WebResourceCollection collection = new WebResourceCollection();
collection.addUrlPattern("/*");
collection.addUrlPattern(constraintUrl);
constraint.addWebResourceCollection(collection);
constraint.addRoleAllowed("manager");
constraint.addRoleAllowed("el-jefe");

View file

@ -0,0 +1,24 @@
<keycloak-saml-adapter>
<SP entityID="http://localhost:8081/input-portal/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false">
<PrincipalNameMapping policy="FROM_NAME_ID"/>
<RoleIdentifiers>
<Attribute name="Role"/>
</RoleIdentifiers>
<IDP entityID="idp">
<SingleSignOnService requestBinding="POST"
bindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
/>
<SingleLogoutService
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
redirectBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
/>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -85,6 +85,23 @@
"saml_idp_initiated_sso_url_name": "sales-post"
}
},
{
"name": "http://localhost:8081/input-portal/",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8081/input-portal/",
"redirectUris": [
"http://localhost:8081/input-portal/*"
],
"attributes": {
"saml.authnstatement": "true",
"saml_assertion_consumer_url_post": "http://localhost:8081/input-portal/",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/input-portal/",
"saml_single_logout_service_url_post": "http://localhost:8081/input-portal/",
"saml_single_logout_service_url_redirect": "http://localhost:8081/input-portal/"
}
},
{
"name": "http://localhost:8081/sales-post-passive/",
"enabled": true,