KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library

KEYCLOAK-2075 - added integration tests for both server and adapter side
This commit is contained in:
Vlastimil Elias 2015-11-19 10:56:32 +01:00
parent 0bdb05e152
commit 18fa03bf97
28 changed files with 318 additions and 164 deletions

View file

@ -11,7 +11,8 @@
sslPolicy="EXTERNAL" sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp" logoutPage="/logout.jsp"
forceAuthentication="false"> forceAuthentication="false"
isPassive="false">
<Keys> <Keys>
<Key signing="true" > <Key signing="true" >
<KeyStore resource="/WEB-INF/keystore.jks" password="store123"> <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
@ -63,7 +64,8 @@
<SP entityID="sp" <SP entityID="sp"
sslPolicy="ssl" sslPolicy="ssl"
nameIDPolicyFormat="format" nameIDPolicyFormat="format"
forceAuthentication="true"> forceAuthentication="true"
isPassive="false">
... ...
</SP>]]></programlisting> </SP>]]></programlisting>
<para> <para>
@ -106,12 +108,23 @@
<listitem> <listitem>
<para> <para>
SAML clients can request that a user is re-authenticated even if SAML clients can request that a user is re-authenticated even if
they are already logged in at the IDP. Set this to true if you they are already logged in at the IDP. Set this to <literal>true</literal> if you
want this. want this.
<emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default. <emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term>isPassive</term>
<listitem>
<para>
SAML clients can request that a user is never asked to authenticate even if
they are not logged in at the IDP. Set this to <literal>true</literal> if you want this.
Do not use together with <literal>forceAuthentication</literal> as they are opposite.
<emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
</para>
</listitem>
</varlistentry>
</variablelist> </variablelist>
</para> </para>
</section> </section>

View file

@ -1,5 +1,5 @@
<chapter id="tomcat-adapter"> <chapter id="tomcat-adapter">
<title>Tomcat 6, 7 and 8 SAML dapters</title> <title>Tomcat 6, 7 and 8 SAML adapters</title>
<para> <para>
To be able to secure WAR apps deployed on Tomcat 6, 7 and 8 you must install the Keycloak Tomcat 6, 7 or 8 SAML adapter To be able to secure WAR apps deployed on Tomcat 6, 7 and 8 you must install the Keycloak Tomcat 6, 7 or 8 SAML adapter
into your Tomcat installation. You then have to provide some extra configuration in each WAR you deploy to into your Tomcat installation. You then have to provide some extra configuration in each WAR you deploy to

View file

@ -5,8 +5,5 @@ package org.keycloak.adapters.spi;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public enum AuthOutcome { public enum AuthOutcome {
NOT_ATTEMPTED, NOT_ATTEMPTED, FAILED, AUTHENTICATED, NOT_AUTHENTICATED, LOGGED_OUT
FAILED,
AUTHENTICATED,
LOGGED_OUT
} }

View file

@ -1,13 +1,13 @@
package org.keycloak.adapters.saml; package org.keycloak.adapters.saml;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.saml.SignatureAlgorithm;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.Set; import java.util.Set;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.saml.SignatureAlgorithm;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -150,12 +150,8 @@ public class DefaultSamlDeployment implements SamlDeployment {
} }
} }
public static class DefaultIDP implements IDP { public static class DefaultIDP implements IDP {
private String entityID; private String entityID;
private PublicKey signatureValidationKey; private PublicKey signatureValidationKey;
private SingleSignOnService singleSignOnService; private SingleSignOnService singleSignOnService;
@ -204,6 +200,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
private String entityID; private String entityID;
private String nameIDPolicyFormat; private String nameIDPolicyFormat;
private boolean forceAuthentication; private boolean forceAuthentication;
private boolean isPassive;
private PrivateKey decryptionKey; private PrivateKey decryptionKey;
private KeyPair signingKeyPair; private KeyPair signingKeyPair;
private String assertionConsumerServiceUrl; private String assertionConsumerServiceUrl;
@ -214,7 +211,6 @@ public class DefaultSamlDeployment implements SamlDeployment {
private SignatureAlgorithm signatureAlgorithm; private SignatureAlgorithm signatureAlgorithm;
private String signatureCanonicalizationMethod; private String signatureCanonicalizationMethod;
@Override @Override
public IDP getIDP() { public IDP getIDP() {
return idp; return idp;
@ -245,6 +241,11 @@ public class DefaultSamlDeployment implements SamlDeployment {
return forceAuthentication; return forceAuthentication;
} }
@Override
public boolean isIsPassive() {
return isPassive;
}
@Override @Override
public PrivateKey getDecryptionKey() { public PrivateKey getDecryptionKey() {
return decryptionKey; return decryptionKey;
@ -299,6 +300,10 @@ public class DefaultSamlDeployment implements SamlDeployment {
this.forceAuthentication = forceAuthentication; this.forceAuthentication = forceAuthentication;
} }
public void setIsPassive(boolean isPassive){
this.isPassive = isPassive;
}
public void setDecryptionKey(PrivateKey decryptionKey) { public void setDecryptionKey(PrivateKey decryptionKey) {
this.decryptionKey = decryptionKey; this.decryptionKey = decryptionKey;
} }

View file

@ -48,7 +48,7 @@ public class InitiateLogin implements AuthChallenge {
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder() SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
.destination(destinationUrl) .destination(destinationUrl)
.issuer(issuerURL) .issuer(issuerURL)
.forceAuthn(deployment.isForceAuthentication()) .forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive())
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat)); .nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) { if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) {
String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get(); String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();

View file

@ -17,7 +17,9 @@ import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType; import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.saml.BaseSAML2BindingBuilder; import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2LogoutRequestBuilder; import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAML2LogoutResponseBuilder;
@ -27,6 +29,7 @@ import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.Base64; import org.keycloak.saml.common.util.Base64;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature; import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil; import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
@ -207,7 +210,26 @@ public abstract class SamlAuthenticator {
log.error("Request URI does not match SAML request destination"); log.error("Request URI does not match SAML request destination");
return AuthOutcome.FAILED; return AuthOutcome.FAILED;
} }
if (statusResponse instanceof ResponseType) { if (statusResponse instanceof ResponseType) {
//validate status
StatusType status = statusResponse.getStatus();
if(status == null){
log.error("Missing Status in SAML response");
return AuthOutcome.FAILED;
}
if(!checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_SUCCESS.get())){
if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
// KEYCLOAK-2107 - handle user not authenticated due passive mode
log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
return AuthOutcome.NOT_AUTHENTICATED;
}
log.error("Error Status found in SAML response: " + status.toString());
return AuthOutcome.FAILED;
}
try { try {
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) { if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
try { try {
@ -287,7 +309,16 @@ public abstract class SamlAuthenticator {
} }
} }
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
if(statusCode != null && statusCode.getValue()!=null){
String v = statusCode.getValue().toString();
return expectedValue.equals(v);
}
return false;
}
protected AuthOutcome handleLoginResponse(ResponseType responseType) { protected AuthOutcome handleLoginResponse(ResponseType responseType) {
AssertionType assertion = null; AssertionType assertion = null;
try { try {
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey()); assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
@ -295,7 +326,7 @@ public abstract class SamlAuthenticator {
return initiateLogin(); return initiateLogin();
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Error extracting SAML assertion, e"); log.error("Error extracting SAML assertion: " + e.getMessage());
challenge = new AuthChallenge() { challenge = new AuthChallenge() {
@Override @Override
public boolean challenge(HttpFacade exchange) { public boolean challenge(HttpFacade exchange) {
@ -434,9 +465,9 @@ public abstract class SamlAuthenticator {
return SAMLRequestParser.parseRequestRedirectBinding(response); return SAMLRequestParser.parseRequestRedirectBinding(response);
} }
protected SAMLDocumentHolder extractPostBindingResponse(String response) { protected SAMLDocumentHolder extractPostBindingResponse(String response) {
byte[] samlBytes = PostBindingUtil.base64Decode(response); byte[] samlBytes = PostBindingUtil.base64Decode(response);
String xml = new String(samlBytes);
return SAMLRequestParser.parseResponseDocument(samlBytes); return SAMLRequestParser.parseResponseDocument(samlBytes);
} }

View file

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

View file

@ -33,6 +33,7 @@ public class SP implements Serializable {
private String entityID; private String entityID;
private String sslPolicy; private String sslPolicy;
private boolean forceAuthentication; private boolean forceAuthentication;
private boolean isPassive;
private String logoutPage; private String logoutPage;
private List<Key> keys; private List<Key> keys;
private String nameIDPolicyFormat; private String nameIDPolicyFormat;
@ -64,6 +65,14 @@ public class SP implements Serializable {
this.forceAuthentication = forceAuthentication; this.forceAuthentication = forceAuthentication;
} }
public boolean isIsPassive() {
return isPassive;
}
public void setIsPassive(boolean isPassive) {
this.isPassive = isPassive;
}
public List<Key> getKeys() { public List<Key> getKeys() {
return keys; return keys;
} }

View file

@ -11,11 +11,11 @@ public class ConfigXmlConstants {
public static final String SSL_POLICY_ATTR = "sslPolicy"; public static final String SSL_POLICY_ATTR = "sslPolicy";
public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat"; public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat";
public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication"; public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication";
public static final String IS_PASSIVE_ATTR = "isPassive";
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm"; public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod"; public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
public static final String LOGOUT_PAGE_ATTR = "logoutPage"; public static final String LOGOUT_PAGE_ATTR = "logoutPage";
public static final String KEYS_ELEMENT = "Keys"; public static final String KEYS_ELEMENT = "Keys";
public static final String KEY_ELEMENT = "Key"; public static final String KEY_ELEMENT = "Key";
public static final String SIGNING_ATTR = "signing"; public static final String SIGNING_ATTR = "signing";
@ -36,7 +36,6 @@ public class ConfigXmlConstants {
public static final String POLICY_ATTR = "policy"; public static final String POLICY_ATTR = "policy";
public static final String ATTRIBUTE_ATTR = "attribute"; public static final String ATTRIBUTE_ATTR = "attribute";
public static final String ROLE_IDENTIFIERS_ELEMENT = "RoleIdentifiers"; public static final String ROLE_IDENTIFIERS_ELEMENT = "RoleIdentifiers";
public static final String ATTRIBUTE_ELEMENT = "Attribute"; public static final String ATTRIBUTE_ELEMENT = "Attribute";
public static final String NAME_ATTR = "name"; public static final String NAME_ATTR = "name";

View file

@ -41,6 +41,7 @@ public class DeploymentBuilder {
deployment.setConfigured(true); deployment.setConfigured(true);
deployment.setEntityID(sp.getEntityID()); deployment.setEntityID(sp.getEntityID());
deployment.setForceAuthentication(sp.isForceAuthentication()); deployment.setForceAuthentication(sp.isForceAuthentication());
deployment.setIsPassive(sp.isIsPassive());
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat()); deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
deployment.setLogoutPage(sp.getLogoutPage()); deployment.setLogoutPage(sp.getLogoutPage());
deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod()); deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());

View file

@ -1,21 +1,22 @@
package org.keycloak.adapters.saml.config.parsers; package org.keycloak.adapters.saml.config.parsers;
import org.keycloak.adapters.saml.config.IDP; import java.util.HashSet;
import org.keycloak.adapters.saml.config.Key; import java.util.List;
import org.keycloak.adapters.saml.config.SP; import java.util.Set;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.parsers.AbstractParser;
import org.keycloak.saml.common.util.StaxParserUtil;
import org.keycloak.common.util.StringPropertyReplacer;
import javax.xml.namespace.QName; import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.EndElement; import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement; import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent; import javax.xml.stream.events.XMLEvent;
import java.util.HashSet;
import java.util.List; import org.keycloak.adapters.saml.config.IDP;
import java.util.Set; import org.keycloak.adapters.saml.config.Key;
import org.keycloak.adapters.saml.config.SP;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.parsers.AbstractParser;
import org.keycloak.saml.common.util.StaxParserUtil;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -25,13 +26,16 @@ public class SPXmlParser extends AbstractParser {
public static String getAttributeValue(StartElement startElement, String tag) { public static String getAttributeValue(StartElement startElement, String tag) {
String str = StaxParserUtil.getAttributeValue(startElement, tag); String str = StaxParserUtil.getAttributeValue(startElement, tag);
if (str != null) return StringPropertyReplacer.replaceProperties(str); if (str != null)
else return str; return StringPropertyReplacer.replaceProperties(str);
else
return str;
} }
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) { public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
String result = getAttributeValue(startElement, tag); String result = getAttributeValue(startElement, tag);
if (result == null) return defaultValue; if (result == null)
return defaultValue;
return Boolean.valueOf(result); return Boolean.valueOf(result);
} }
@ -41,11 +45,11 @@ public class SPXmlParser extends AbstractParser {
public static String getElementText(XMLEventReader xmlEventReader) throws ParsingException { public static String getElementText(XMLEventReader xmlEventReader) throws ParsingException {
String result = StaxParserUtil.getElementText(xmlEventReader); String result = StaxParserUtil.getElementText(xmlEventReader);
if (result != null) result = StringPropertyReplacer.replaceProperties(result); if (result != null)
result = StringPropertyReplacer.replaceProperties(result);
return result; return result;
} }
@Override @Override
public Object parse(XMLEventReader xmlEventReader) throws ParsingException { public Object parse(XMLEventReader xmlEventReader) throws ParsingException {
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader); StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
@ -61,6 +65,7 @@ public class SPXmlParser extends AbstractParser {
sp.setLogoutPage(getAttributeValue(startElement, ConfigXmlConstants.LOGOUT_PAGE_ATTR)); sp.setLogoutPage(getAttributeValue(startElement, ConfigXmlConstants.LOGOUT_PAGE_ATTR));
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR)); sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR)); sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
while (xmlEventReader.hasNext()) { while (xmlEventReader.hasNext()) {
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader); XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
if (xmlEvent == null) if (xmlEvent == null)
@ -144,7 +149,6 @@ public class SPXmlParser extends AbstractParser {
sp.setRoleAttributes(roleAttributes); sp.setRoleAttributes(roleAttributes);
} }
@Override @Override
public boolean supports(QName qname) { public boolean supports(QName qname) {
return false; return false;

View file

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

View file

@ -68,6 +68,7 @@ public class XmlParserTest {
Assert.assertEquals("ssl", sp.getSslPolicy()); Assert.assertEquals("ssl", sp.getSslPolicy());
Assert.assertEquals("format", sp.getNameIDPolicyFormat()); Assert.assertEquals("format", sp.getNameIDPolicyFormat());
Assert.assertTrue(sp.isForceAuthentication()); Assert.assertTrue(sp.isForceAuthentication());
Assert.assertTrue(sp.isIsPassive());
Assert.assertEquals(2, sp.getKeys().size()); Assert.assertEquals(2, sp.getKeys().size());
Key signing = sp.getKeys().get(0); Key signing = sp.getKeys().get(0);
Assert.assertTrue(signing.isSigning()); Assert.assertTrue(signing.isSigning());

View file

@ -2,7 +2,8 @@
<SP entityID="sp" <SP entityID="sp"
sslPolicy="ssl" sslPolicy="ssl"
nameIDPolicyFormat="format" nameIDPolicyFormat="format"
forceAuthentication="true"> forceAuthentication="true"
isPassive="true">
<Keys> <Keys>
<Key signing="true" > <Key signing="true" >
<KeyStore file="file" resource="cp" password="pw"> <KeyStore file="file" resource="cp" password="pw">

View file

@ -4,7 +4,8 @@
nameIDPolicyFormat="format" nameIDPolicyFormat="format"
signatureAlgorithm="" signatureAlgorithm=""
signatureCanonicalizationMethod="" signatureCanonicalizationMethod=""
forceAuthentication="true"> forceAuthentication="true"
isPassive="true">
<Keys> <Keys>
<Key signing="true" > <Key signing="true" >
<KeyStore file="file" resource="cp" password="pw"> <KeyStore file="file" resource="cp" password="pw">

View file

@ -1,18 +1,11 @@
package org.keycloak.adapters.saml.servlet; package org.keycloak.adapters.saml.servlet;
import org.keycloak.adapters.spi.AuthChallenge; import java.io.FileInputStream;
import org.keycloak.adapters.spi.AuthOutcome; import java.io.FileNotFoundException;
import org.keycloak.adapters.spi.InMemorySessionIdMapper; import java.io.IOException;
import org.keycloak.adapters.spi.SessionIdMapper; import java.io.InputStream;
import org.keycloak.adapters.saml.DefaultSamlDeployment; import java.util.logging.Level;
import org.keycloak.adapters.saml.SamlAuthenticator; import java.util.logging.Logger;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
import org.keycloak.adapters.servlet.ServletHttpFacade;
import org.keycloak.saml.common.exceptions.ParsingException;
import javax.servlet.Filter; import javax.servlet.Filter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@ -24,12 +17,20 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import org.keycloak.adapters.saml.DefaultSamlDeployment;
import java.io.IOException; import org.keycloak.adapters.saml.SamlAuthenticator;
import java.io.InputStream; import org.keycloak.adapters.saml.SamlDeployment;
import java.util.logging.Level; import org.keycloak.adapters.saml.SamlDeploymentContext;
import java.util.logging.Logger; import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
import org.keycloak.adapters.servlet.ServletHttpFacade;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.saml.common.exceptions.ParsingException;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -46,9 +47,11 @@ public class SamlFilter implements Filter {
if (configResolverClass != null) { if (configResolverClass != null) {
try { try {
throw new RuntimeException("Not implemented yet"); throw new RuntimeException("Not implemented yet");
//KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); // KeycloakConfigResolver configResolver = (KeycloakConfigResolver)
// context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
// deploymentContext = new SamlDeploymentContext(configResolver); // deploymentContext = new SamlDeploymentContext(configResolver);
//log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); // log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.",
// configResolverClass);
} catch (Exception ex) { } catch (Exception ex) {
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() }); log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() });
// deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); // deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
@ -65,7 +68,8 @@ public class SamlFilter implements Filter {
} else { } else {
String path = "/WEB-INF/keycloak-saml.xml"; String path = "/WEB-INF/keycloak-saml.xml";
String pathParam = filterConfig.getInitParameter("keycloak.config.path"); String pathParam = filterConfig.getInitParameter("keycloak.config.path");
if (pathParam != null) path = pathParam; if (pathParam != null)
path = pathParam;
is = filterConfig.getServletContext().getResourceAsStream(path); is = filterConfig.getServletContext().getResourceAsStream(path);
} }
final SamlDeployment deployment; final SamlDeployment deployment;
@ -105,7 +109,6 @@ public class SamlFilter implements Filter {
} }
FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper); FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper);
SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) { SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) {
@Override @Override
protected void completeAuthentication(SamlSession account) { protected void completeAuthentication(SamlSession account) {
@ -139,6 +142,16 @@ public class SamlFilter implements Filter {
challenge.challenge(facade); challenge.challenge(facade);
return; return;
} }
if (deployment.isIsPassive() && outcome == AuthOutcome.NOT_AUTHENTICATED) {
log.fine("PASSIVE_NOT_AUTHENTICATED");
if (facade.isEnded()) {
return;
}
chain.doFilter(req, res);
return;
}
if (!facade.isEnded()) { if (!facade.isEnded()) {
response.sendError(403); response.sendError(403);
} }

View file

@ -16,6 +16,15 @@
*/ */
package org.keycloak.adapters.saml.undertow; package org.keycloak.adapters.saml.undertow;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.undertow.UndertowHttpFacade;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.NotificationReceiver; import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext; import io.undertow.security.api.SecurityContext;
@ -24,14 +33,6 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey; import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers; import io.undertow.util.Headers;
import io.undertow.util.StatusCodes; import io.undertow.util.StatusCodes;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.undertow.UndertowHttpFacade;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
/** /**
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism. * Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
@ -44,8 +45,7 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
protected UndertowUserSessionManagement sessionManagement; protected UndertowUserSessionManagement sessionManagement;
protected String errorPage; protected String errorPage;
public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) {
String errorPage) {
this.deploymentContext = deploymentContext; this.deploymentContext = deploymentContext;
this.sessionManagement = sessionManagement; this.sessionManagement = sessionManagement;
this.errorPage = errorPage; this.errorPage = errorPage;
@ -69,19 +69,19 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
} }
static void sendRedirect(final HttpServerExchange exchange, final String location) { static void sendRedirect(final HttpServerExchange exchange, final String location) {
// TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this. // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better
// handle this.
String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location; String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location;
exchange.getResponseHeaders().put(Headers.LOCATION, loc); exchange.getResponseHeaders().put(Headers.LOCATION, loc);
} }
protected void registerNotifications(final SecurityContext securityContext) { protected void registerNotifications(final SecurityContext securityContext) {
final NotificationReceiver logoutReceiver = new NotificationReceiver() { final NotificationReceiver logoutReceiver = new NotificationReceiver() {
@Override @Override
public void handleNotification(SecurityNotification notification) { public void handleNotification(SecurityNotification notification) {
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return; if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT)
return;
HttpServerExchange exchange = notification.getExchange(); HttpServerExchange exchange = notification.getExchange();
UndertowHttpFacade facade = createFacade(exchange); UndertowHttpFacade facade = createFacade(exchange);
@ -104,13 +104,16 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
return AuthenticationMechanismOutcome.NOT_ATTEMPTED; return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
} }
SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext); SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext);
UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade, UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade, deploymentContext.resolveDeployment(facade), sessionStore);
deploymentContext.resolveDeployment(facade), sessionStore);
AuthOutcome outcome = authenticator.authenticate(); AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) { if (outcome == AuthOutcome.AUTHENTICATED) {
registerNotifications(securityContext); registerNotifications(securityContext);
return AuthenticationMechanismOutcome.AUTHENTICATED; return AuthenticationMechanismOutcome.AUTHENTICATED;
} }
if (outcome == AuthOutcome.NOT_AUTHENTICATED) {
// we are in passive mode and user is not authenticated, let app server to try another auth mechanism
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
if (outcome == AuthOutcome.LOGGED_OUT) { if (outcome == AuthOutcome.LOGGED_OUT) {
securityContext.logout(); securityContext.logout();
if (deployment.getLogoutPage() != null) { if (deployment.getLogoutPage() != null) {

View file

@ -80,4 +80,10 @@ public class StatusCodeType implements Serializable {
public void setValue(URI value) { public void setValue(URI value) {
this.value = value; this.value = value;
} }
@Override
public String toString() {
return "StatusCodeType [value=" + value + ", statusCode=" + statusCode + "]";
}
} }

View file

@ -100,4 +100,9 @@ public class StatusType implements Serializable {
this.statusDetail = value; this.statusDetail = value;
} }
@Override
public String toString() {
return "StatusType [statusCode=" + statusCode + ", statusMessage=" + statusMessage + ", statusDetail=" + statusDetail + "]";
}
} }

View file

@ -17,16 +17,16 @@
*/ */
package org.keycloak.saml; package org.keycloak.saml;
import java.net.URI;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator; import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import java.net.URI;
/** /**
* @author pedroigor * @author pedroigor
*/ */
@ -64,6 +64,11 @@ public class SAML2AuthnRequestBuilder {
return this; return this;
} }
public SAML2AuthnRequestBuilder isPassive(boolean isPassive) {
this.authnRequestType.setIsPassive(isPassive);
return this;
}
public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) { public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) {
this.authnRequestType.setNameIDPolicy(nameIDPolicy.build()); this.authnRequestType.setNameIDPolicy(nameIDPolicy.build());
return this; return this;

View file

@ -1,21 +1,28 @@
package org.keycloak.saml; package org.keycloak.saml;
<<<<<<< Upstream, based on keycloak/master
import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.StatusCodeType; import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusType; import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
=======
import org.keycloak.dom.saml.v2.protocol.ResponseType;
>>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side
import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response; import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator; import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.factories.JBossSAMLAuthnResponseFactory; import org.keycloak.saml.processing.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
<<<<<<< Upstream, based on keycloak/master
import org.keycloak.saml.processing.core.saml.v2.holders.IDPInfoHolder; import org.keycloak.saml.processing.core.saml.v2.holders.IDPInfoHolder;
import org.keycloak.saml.processing.core.saml.v2.holders.IssuerInfoHolder; import org.keycloak.saml.processing.core.saml.v2.holders.IssuerInfoHolder;
import org.keycloak.saml.processing.core.saml.v2.holders.SPInfoHolder; import org.keycloak.saml.processing.core.saml.v2.holders.SPInfoHolder;
import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
=======
>>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side
import org.w3c.dom.Document; import org.w3c.dom.Document;
import java.net.URI; import java.net.URI;
@ -45,7 +52,6 @@ public class SAML2ErrorResponseBuilder {
return this; return this;
} }
public Document buildDocument() throws ProcessingException { public Document buildDocument() throws ProcessingException {
try { try {
@ -65,8 +71,6 @@ public class SAML2ErrorResponseBuilder {
} catch (ParsingException e) { } catch (ParsingException e) {
throw new ProcessingException(e); throw new ProcessingException(e);
} }
} }
} }

View file

@ -147,8 +147,7 @@ public class SamlProtocol implements LoginProtocol {
@Override @Override
public Response sendError(ClientSessionModel clientSession, Error error) { public Response sendError(ClientSessionModel clientSession, Error error) {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); try {
session.sessions().removeClientSession(realm, clientSession);
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) { if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
if (error == Error.CANCELLED_BY_USER) { if (error == Error.CANCELLED_BY_USER) {
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO"); UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
@ -175,6 +174,10 @@ public class SamlProtocol implements LoginProtocol {
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
} }
} }
} finally {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
session.sessions().removeClientSession(realm, clientSession);
}
} }
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) { private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {

View file

@ -19,6 +19,7 @@ public class SamlAdapterTest {
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader(); ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
@ -96,6 +97,11 @@ public class SamlAdapterTest {
testStrategy.testPostSimpleLoginLogout(); testStrategy.testPostSimpleLoginLogout();
} }
@Test
public void testPostPassiveLoginLogout() {
testStrategy.testPostPassiveLoginLogout(true);
}
@Test @Test
public void testPostSignedLoginLogoutTransientNameID() { public void testPostSignedLoginLogoutTransientNameID() {
testStrategy.testPostSignedLoginLogoutTransientNameID(); testStrategy.testPostSignedLoginLogoutTransientNameID();

View file

@ -139,6 +139,37 @@ public class SamlAdapterTestStrategy extends ExternalResource {
checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post/"); checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post/");
} }
public void testPostPassiveLoginLogout(boolean forbiddenIfNotauthenticated) {
// first request on passive app - no login page shown, user not logged in as we are in passive mode
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
System.out.println(driver.getPageSource());
if (forbiddenIfNotauthenticated) {
Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
} else {
Assert.assertTrue(driver.getPageSource().contains("principal=null"));
}
// login user by asking login from other app
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
loginPage.login("bburke", "password");
// navigate to the passive app again, we have to be logged in now
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
System.out.println(driver.getPageSource());
Assert.assertTrue(driver.getPageSource().contains("bburke"));
// logout from both app
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive?GLO=true");
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true");
// refresh passive app page, not logged in again as we are in passive mode
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
Assert.assertFalse(driver.getPageSource().contains("bburke"));
}
public void testPostSimpleUnauthorized(CheckAuthError error) { public void testPostSimpleUnauthorized(CheckAuthError error) {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/"); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");

View file

@ -1,35 +1,24 @@
package org.keycloak.testsuite.saml; package org.keycloak.testsuite.saml;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.junit.Assert; import org.junit.Assert;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.Config;
import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper; import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
import org.keycloak.protocol.saml.mappers.HardcodedRole; import org.keycloak.protocol.saml.mappers.HardcodedRole;
import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.RoleNameMapper; import org.keycloak.protocol.saml.mappers.RoleNameMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule; import org.keycloak.testsuite.rule.WebRule;
@ -47,19 +36,10 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -166,6 +146,7 @@ public class SamlBindingTest {
driver.navigate().to("http://localhost:8081/sales-post?GLO=true"); driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
checkLoggedOut("http://localhost:8081/sales-post/"); checkLoggedOut("http://localhost:8081/sales-post/");
} }
@Test @Test
public void testPostSimpleLoginLogoutIdpInitiated() { public void testPostSimpleLoginLogoutIdpInitiated() {
driver.navigate().to("http://localhost:8081/auth/realms/demo/protocol/saml/clients/sales-post"); driver.navigate().to("http://localhost:8081/auth/realms/demo/protocol/saml/clients/sales-post");
@ -188,6 +169,7 @@ public class SamlBindingTest {
checkLoggedOut("http://localhost:8081/sales-post-sig/"); checkLoggedOut("http://localhost:8081/sales-post-sig/");
} }
@Test @Test
public void testPostSignedLoginLogoutTransientNameID() { public void testPostSignedLoginLogoutTransientNameID() {
driver.navigate().to("http://localhost:8081/sales-post-sig-transient/"); driver.navigate().to("http://localhost:8081/sales-post-sig-transient/");
@ -452,23 +434,10 @@ public class SamlBindingTest {
Assert.assertTrue(driver.getPageSource().contains("null")); Assert.assertTrue(driver.getPageSource().contains("null"));
} }
private static String createToken() { @Test
KeycloakSession session = keycloakRule.startSession(); public void testPassiveMode() {
try { // KEYCLOAK-2075 test SAML IsPassive handling - PicketLink SP client library doesn't support this option unfortunately.
RealmManager manager = new RealmManager(session); // But the test of server side is included in test of SAML Keycloak adapter
RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
ClientModel adminConsole = adminRealm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
TokenManager tm = new TokenManager();
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master");
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
return tm.encodeToken(adminRealm, token);
} finally {
keycloakRule.stopSession(session, true);
}
} }

View file

@ -5,7 +5,6 @@ import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy; import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy;
import org.keycloak.testsuite.keycloaksaml.SamlSPFacade;
import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet; import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
@ -25,6 +24,7 @@ public class SamlAdapterTest {
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader(); ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
@ -37,9 +37,6 @@ public class SamlAdapterTest {
initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-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); initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth"); SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
} }
@Override @Override
@ -105,6 +102,11 @@ public class SamlAdapterTest {
testStrategy.testPostSimpleLoginLogout(); testStrategy.testPostSimpleLoginLogout();
} }
@Test
public void testPostPassiveLoginLogout() {
testStrategy.testPostPassiveLoginLogout(false);
}
@Test @Test
public void testPostSignedLoginLogoutTransientNameID() { public void testPostSignedLoginLogoutTransientNameID() {
testStrategy.testPostSignedLoginLogoutTransientNameID(); testStrategy.testPostSignedLoginLogoutTransientNameID();

View file

@ -0,0 +1,25 @@
<keycloak-saml-adapter>
<SP entityID="http://localhost:8081/sales-post-passive/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false"
isPassive="true">
<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,24 @@
"saml_idp_initiated_sso_url_name": "sales-post" "saml_idp_initiated_sso_url_name": "sales-post"
} }
}, },
{
"name": "http://localhost:8081/sales-post-passive/",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8081/sales-post-passive",
"redirectUris": [
"http://localhost:8081/sales-post-passive/*"
],
"attributes": {
"saml.authnstatement": "true",
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-passive/",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-passive/",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post-passive/",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-passive/",
"saml_idp_initiated_sso_url_name": "sales-post-passive"
}
},
{ {
"name": "http://localhost:8081/sales-post-sig/", "name": "http://localhost:8081/sales-post-sig/",
"enabled": true, "enabled": true,