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 $
@ -31,7 +31,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
return validateResponseSignature; return validateResponseSignature;
} }
@Override @Override
public Binding getRequestBinding() { public Binding getRequestBinding() {
return requestBinding; return requestBinding;
} }
@ -97,7 +97,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
return signResponse; return signResponse;
} }
@Override @Override
public Binding getRequestBinding() { public Binding getRequestBinding() {
return requestBinding; return requestBinding;
} }
@ -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;
@ -244,6 +240,11 @@ public class DefaultSamlDeployment implements SamlDeployment {
public boolean isForceAuthentication() { public boolean isForceAuthentication() {
return forceAuthentication; return forceAuthentication;
} }
@Override
public boolean isIsPassive() {
return isPassive;
}
@Override @Override
public PrivateKey getDecryptionKey() { public PrivateKey getDecryptionKey() {
@ -265,7 +266,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
return roleAttributeNames; return roleAttributeNames;
} }
@Override @Override
public PrincipalNamePolicy getPrincipalNamePolicy() { public PrincipalNamePolicy getPrincipalNamePolicy() {
return principalNamePolicy; return principalNamePolicy;
} }
@ -298,6 +299,10 @@ public class DefaultSamlDeployment implements SamlDeployment {
public void setForceAuthentication(boolean forceAuthentication) { public void setForceAuthentication(boolean forceAuthentication) {
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;
@ -332,7 +337,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
this.logoutPage = logoutPage; this.logoutPage = logoutPage;
} }
@Override @Override
public String getSignatureCanonicalizationMethod() { public String getSignatureCanonicalizationMethod() {
return signatureCanonicalizationMethod; return signatureCanonicalizationMethod;
} }

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

@ -5,17 +5,17 @@ package org.keycloak.adapters.saml.config.parsers;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ConfigXmlConstants { public class ConfigXmlConstants {
public static final String KEYCLOAK_SAML_ADAPTER ="keycloak-saml-adapter"; public static final String KEYCLOAK_SAML_ADAPTER = "keycloak-saml-adapter";
public static final String SP_ELEMENT="SP"; public static final String SP_ELEMENT = "SP";
public static final String ENTITY_ID_ATTR = "entityID"; public static final String ENTITY_ID_ATTR = "entityID";
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)
@ -79,7 +84,7 @@ public class SPXmlParser extends AbstractParser {
String tag = StaxParserUtil.getStartElementName(startElement); String tag = StaxParserUtil.getStartElementName(startElement);
if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) { if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
KeysXmlParser parser = new KeysXmlParser(); KeysXmlParser parser = new KeysXmlParser();
List<Key> keys = (List<Key>)parser.parse(xmlEventReader); List<Key> keys = (List<Key>) parser.parse(xmlEventReader);
sp.setKeys(keys); sp.setKeys(keys);
} else if (tag.equals(ConfigXmlConstants.PRINCIPAL_NAME_MAPPING_ELEMENT)) { } else if (tag.equals(ConfigXmlConstants.PRINCIPAL_NAME_MAPPING_ELEMENT)) {
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader); StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
@ -98,7 +103,7 @@ public class SPXmlParser extends AbstractParser {
parseRoleMapping(xmlEventReader, sp); parseRoleMapping(xmlEventReader, sp);
} else if (tag.equals(ConfigXmlConstants.IDP_ELEMENT)) { } else if (tag.equals(ConfigXmlConstants.IDP_ELEMENT)) {
IDPXmlParser parser = new IDPXmlParser(); IDPXmlParser parser = new IDPXmlParser();
IDP idp = (IDP)parser.parse(xmlEventReader); IDP idp = (IDP) parser.parse(xmlEventReader);
sp.setIdp(idp); sp.setIdp(idp);
} else { } else {
StaxParserUtil.bypassElementBlock(xmlEventReader, tag); StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
@ -108,7 +113,7 @@ public class SPXmlParser extends AbstractParser {
return sp; return sp;
} }
protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException { protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException {
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader); StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
StaxParserUtil.validate(startElement, ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT); StaxParserUtil.validate(startElement, ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT);
Set<String> roleAttributes = new HashSet<>(); Set<String> roleAttributes = new HashSet<>();
@ -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>
@ -38,7 +39,7 @@ import java.util.logging.Logger;
public class SamlFilter implements Filter { public class SamlFilter implements Filter {
protected SamlDeploymentContext deploymentContext; protected SamlDeploymentContext deploymentContext;
protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
private final static Logger log = Logger.getLogger(""+SamlFilter.class); private final static Logger log = Logger.getLogger("" + SamlFilter.class);
@Override @Override
public void init(final FilterConfig filterConfig) throws ServletException { public void init(final FilterConfig filterConfig) throws ServletException {
@ -46,12 +47,14 @@ 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)
//deploymentContext = new SamlDeploymentContext(configResolver); // context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
//log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); // deploymentContext = new SamlDeploymentContext(configResolver);
// 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());
} }
} else { } else {
String fp = filterConfig.getInitParameter("keycloak.config.file"); String fp = filterConfig.getInitParameter("keycloak.config.file");
@ -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;
@ -74,7 +79,7 @@ public class SAML2AuthnRequestBuilder {
return this; return this;
} }
public Document toDocument() { public Document toDocument() {
try { try {
AuthnRequestType authnRequestType = this.authnRequestType; AuthnRequestType authnRequestType = this.authnRequestType;

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,33 +147,36 @@ 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"); Map<String, String> params = new HashMap<>();
Map<String, String> params = new HashMap<>(); params.put("realm", realm.getName());
params.put("realm", realm.getName()); params.put("protocol", LOGIN_PROTOCOL);
params.put("protocol", LOGIN_PROTOCOL); params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME)); URI redirect = builder.buildFromMap(params);
URI redirect = builder.buildFromMap(params); return Response.status(302).location(redirect).build();
return Response.status(302).location(redirect).build();
} else {
return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error));
}
} else {
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
try {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
Document document = builder.buildDocument();
if (isPostBinding(clientSession)) {
return binding.postBinding(document).response(clientSession.getRedirectUri());
} else { } else {
return binding.redirectBinding(document).response(clientSession.getRedirectUri()); return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error));
}
} else {
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
try {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
Document document = builder.buildDocument();
if (isPostBinding(clientSession)) {
return binding.postBinding(document).response(clientSession.getRedirectUri());
} else {
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
}
} catch (Exception e) {
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
} }
} catch (Exception e) {
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
} }
} finally {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
session.sessions().removeClientSession(realm, clientSession);
} }
} }

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,