Merge pull request #1840 from velias/KEYCLOAK-2075

KEYCLOAK-2075 KEYCLOAK-2107 - support for SAML IsPassive mode
This commit is contained in:
Bill Burke 2015-11-25 10:35:00 -05:00
commit d6e2bccb16
38 changed files with 685 additions and 567 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

@ -36,4 +36,11 @@ public class OIDCAuthenticationError implements AuthenticationError {
public String getDescription() { public String getDescription() {
return description; return description;
} }
@Override
public String toString() {
return "OIDCAuthenticationError [reason=" + reason + ", description=" + description + "]";
}
} }

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

@ -40,4 +40,10 @@ public class SamlAuthenticationError implements AuthenticationError {
public StatusResponseType getStatus() { public StatusResponseType getStatus() {
return status; return status;
} }
@Override
public String toString() {
return "SamlAuthenticationError [reason=" + reason + ", status=" + status + "]";
}
} }

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,8 @@ 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) {
try { try {
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) { if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
try { try {
@ -254,7 +258,15 @@ public abstract class SamlAuthenticator {
} }
} else if (sessionStore.isLoggingIn()) { } else if (sessionStore.isLoggingIn()) {
try { try {
// KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly.
StatusType status = statusResponse.getStatus();
if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
return AuthOutcome.NOT_AUTHENTICATED;
}
challenge = new AuthChallenge() { challenge = new AuthChallenge() {
@Override @Override
public boolean challenge(HttpFacade exchange) { public boolean challenge(HttpFacade exchange) {
@ -287,7 +299,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 +316,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 +455,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,5 +1,22 @@
package org.keycloak.protocol.saml; package org.keycloak.protocol.saml;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
@ -36,30 +53,14 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
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.services.ErrorPage;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.admin.ClientAttributeCertificateResource; import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
import org.keycloak.services.ErrorPage;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/** /**
* @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 $
@ -67,7 +68,6 @@ import java.util.UUID;
public class SamlProtocol implements LoginProtocol { public class SamlProtocol implements LoginProtocol {
protected static final Logger logger = Logger.getLogger(SamlProtocol.class); protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
public static final String ATTRIBUTE_TRUE_VALUE = "true"; public static final String ATTRIBUTE_TRUE_VALUE = "true";
public static final String ATTRIBUTE_FALSE_VALUE = "false"; public static final String ATTRIBUTE_FALSE_VALUE = "false";
public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE; public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
@ -115,7 +115,6 @@ public class SamlProtocol implements LoginProtocol {
protected EventBuilder event; protected EventBuilder event;
@Override @Override
public SamlProtocol setSession(KeycloakSession session) { public SamlProtocol setSession(KeycloakSession session) {
this.session = session; this.session = session;
@ -135,7 +134,7 @@ public class SamlProtocol implements LoginProtocol {
} }
@Override @Override
public SamlProtocol setHttpHeaders(HttpHeaders headers){ public SamlProtocol setHttpHeaders(HttpHeaders headers) {
this.headers = headers; this.headers = headers;
return this; return this;
} }
@ -146,22 +145,65 @@ public class SamlProtocol implements LoginProtocol {
return this; return this;
} }
@Override @Override
public Response cancelLogin(ClientSessionModel clientSession) { public Response sendError(ClientSessionModel clientSession, Error error) {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); try {
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) { if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO"); if (error == Error.CANCELLED_BY_USER) {
Map<String, String> params = new HashMap<>(); UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
params.put("realm", realm.getName()); Map<String, String> params = new HashMap<>();
params.put("protocol", LOGIN_PROTOCOL); params.put("realm", realm.getName());
params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME)); params.put("protocol", LOGIN_PROTOCOL);
params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
URI redirect = builder.buildFromMap(params);
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 {
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
}
} 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); session.sessions().removeClientSession(realm, clientSession);
URI redirect = builder.buildFromMap(params); }
return Response.status(302).location(redirect).build(); }
} else {
session.sessions().removeClientSession(realm, clientSession); private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()); switch (error) {
case CANCELLED_BY_USER:
case CONSENT_DENIED:
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
case PASSIVE_INTERACTION_REQUIRED:
case PASSIVE_LOGIN_REQUIRED:
return JBossSAMLURIConstants.STATUS_NO_PASSIVE;
default:
logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
}
}
private String translateErrorToIdpInitiatedErrorMessage(Error error) {
switch (error) {
case CONSENT_DENIED:
return Messages.CONSENT_DENIED;
case PASSIVE_INTERACTION_REQUIRED:
case PASSIVE_LOGIN_REQUIRED:
return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST;
default:
logger.warn("Untranslated protocol Error: " + error.name() + " so we return default error message");
return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST;
} }
} }
@ -169,25 +211,6 @@ public class SamlProtocol implements LoginProtocol {
return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(); return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
} }
protected Response getErrorResponse(ClientSessionModel clientSession, String status) {
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder()
.destination(clientSession.getRedirectUri())
.issuer(getResponseIssuer(realm))
.status(status);
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);
}
}
protected boolean isPostBinding(ClientSessionModel clientSession) { protected boolean isPostBinding(ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient(); ClientModel client = clientSession.getClient();
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || forcePostBinding(client); return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || forcePostBinding(client);
@ -198,8 +221,6 @@ public class SamlProtocol implements LoginProtocol {
return SamlProtocol.SAML_POST_BINDING.equals(note); return SamlProtocol.SAML_POST_BINDING.equals(note);
} }
protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) { protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient(); ClientModel client = clientSession.getClient();
String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE); String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
@ -207,7 +228,8 @@ public class SamlProtocol implements LoginProtocol {
if (logoutPostUrl == null) { if (logoutPostUrl == null) {
// if we don't have a redirect uri either, return true and default to the admin url + POST binding // if we don't have a redirect uri either, return true and default to the admin url + POST binding
if (logoutRedirectUrl == null) return true; if (logoutRedirectUrl == null)
return true;
return false; return false;
} }
@ -218,11 +240,13 @@ public class SamlProtocol implements LoginProtocol {
String bindingType = clientSession.getNote(SAML_BINDING); String bindingType = clientSession.getNote(SAML_BINDING);
// if the login binding was POST, return true // if the login binding was POST, return true
if (SAML_POST_BINDING.equals(bindingType)) return true; if (SAML_POST_BINDING.equals(bindingType))
return true;
if (logoutRedirectUrl == null) return true; // we don't have a redirect binding url, so use post binding if (logoutRedirectUrl == null)
return true; // we don't have a redirect binding url, so use post binding
return false; // redirect binding return false; // redirect binding
} }
@ -248,7 +272,8 @@ public class SamlProtocol implements LoginProtocol {
nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get(); nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get();
} }
} }
if(nameIdFormat == null) return SAML_DEFAULT_NAMEID_FORMAT; if (nameIdFormat == null)
return SAML_DEFAULT_NAMEID_FORMAT;
return nameIdFormat; return nameIdFormat;
} }
@ -259,20 +284,21 @@ public class SamlProtocol implements LoginProtocol {
protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) { protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) {
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) { if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
return userSession.getUser().getEmail(); return userSession.getUser().getEmail();
} else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) { } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) {
// "G-" stands for "generated" Add this for the slight possibility of collisions. // "G-" stands for "generated" Add this for the slight possibility of collisions.
return "G-" + UUID.randomUUID().toString(); return "G-" + UUID.randomUUID().toString();
} else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())) { } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())) {
// generate a persistent user id specifically for each client. // generate a persistent user id specifically for each client.
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId(); String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId();
String samlPersistentId = user.getFirstAttribute(name); String samlPersistentId = user.getFirstAttribute(name);
if (samlPersistentId != null) return samlPersistentId; if (samlPersistentId != null)
return samlPersistentId;
// "G-" stands for "generated" // "G-" stands for "generated"
samlPersistentId = "G-" + UUID.randomUUID().toString(); samlPersistentId = "G-" + UUID.randomUUID().toString();
user.setSingleAttribute(name, samlPersistentId); user.setSingleAttribute(name, samlPersistentId);
return samlPersistentId; return samlPersistentId;
} else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())){ } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
// TODO: Support for persistent NameID (pseudo-random identifier persisted in user object) // TODO: Support for persistent NameID (pseudo-random identifier persisted in user object)
return userSession.getUser().getUsername(); return userSession.getUser().getUsername();
} else { } else {
@ -297,15 +323,8 @@ public class SamlProtocol implements LoginProtocol {
clientSession.setNote(SAML_NAME_ID_FORMAT, nameIdFormat); clientSession.setNote(SAML_NAME_ID_FORMAT, nameIdFormat);
SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder(); SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder();
builder.requestID(requestID) builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan()).sessionIndex(clientSession.getId())
.destination(redirectUri) .requestIssuer(clientSession.getClient().getClientId()).nameIdentifier(nameIdFormat, nameId).authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
.issuer(responseIssuer)
.assertionExpiration(realm.getAccessCodeLifespan())
.subjectExpiration(realm.getAccessTokenLifespan())
.sessionIndex(clientSession.getId())
.requestIssuer(clientSession.getClient().getClientId())
.nameIdentifier(nameIdFormat, nameId)
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
if (!includeAuthnStatement(client)) { if (!includeAuthnStatement(client)) {
builder.disableAuthnStatement(true); builder.disableAuthnStatement(true);
} }
@ -317,20 +336,20 @@ public class SamlProtocol implements LoginProtocol {
Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers(); Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers();
for (ProtocolMapperModel mapping : mappings) { for (ProtocolMapperModel mapping : mappings) {
ProtocolMapper mapper = (ProtocolMapper)session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
if (mapper == null) continue; if (mapper == null)
continue;
if (mapper instanceof SAMLAttributeStatementMapper) { if (mapper instanceof SAMLAttributeStatementMapper) {
attributeStatementMappers.add(new ProtocolMapperProcessor<SAMLAttributeStatementMapper>((SAMLAttributeStatementMapper)mapper, mapping)); attributeStatementMappers.add(new ProtocolMapperProcessor<SAMLAttributeStatementMapper>((SAMLAttributeStatementMapper) mapper, mapping));
} }
if (mapper instanceof SAMLLoginResponseMapper) { if (mapper instanceof SAMLLoginResponseMapper) {
loginResponseMappers.add(new ProtocolMapperProcessor<SAMLLoginResponseMapper>((SAMLLoginResponseMapper)mapper, mapping)); loginResponseMappers.add(new ProtocolMapperProcessor<SAMLLoginResponseMapper>((SAMLLoginResponseMapper) mapper, mapping));
} }
if (mapper instanceof SAMLRoleListMapper) { if (mapper instanceof SAMLRoleListMapper) {
roleListMapper = new ProtocolMapperProcessor<SAMLRoleListMapper>((SAMLRoleListMapper)mapper, mapping); roleListMapper = new ProtocolMapperProcessor<SAMLRoleListMapper>((SAMLRoleListMapper) mapper, mapping);
} }
} }
Document samlDocument = null; Document samlDocument = null;
try { try {
ResponseType samlModel = builder.buildModel(); ResponseType samlModel = builder.buildModel();
@ -351,18 +370,14 @@ public class SamlProtocol implements LoginProtocol {
if (canonicalization != null) { if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization); bindingBuilder.canonicalizationMethod(canonicalization);
} }
bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)) bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
} }
if (requiresAssertionSignature(client)) { if (requiresAssertionSignature(client)) {
String canonicalization = client.getAttribute(SAML_CANONICALIZATION_METHOD_ATTRIBUTE); String canonicalization = client.getAttribute(SAML_CANONICALIZATION_METHOD_ATTRIBUTE);
if (canonicalization != null) { if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization); bindingBuilder.canonicalizationMethod(canonicalization);
} }
bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)) bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signAssertions();
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signAssertions();
} }
if (requiresEncryption(client)) { if (requiresEncryption(client)) {
PublicKey publicKey = null; PublicKey publicKey = null;
@ -402,7 +417,8 @@ public class SamlProtocol implements LoginProtocol {
String alg = client.getAttribute(SAML_SIGNATURE_ALGORITHM); String alg = client.getAttribute(SAML_SIGNATURE_ALGORITHM);
if (alg != null) { if (alg != null) {
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg); SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg);
if (algorithm != null) return algorithm; if (algorithm != null)
return algorithm;
} }
return SignatureAlgorithm.RSA_SHA256; return SignatureAlgorithm.RSA_SHA256;
} }
@ -421,10 +437,8 @@ public class SamlProtocol implements LoginProtocol {
} }
} }
public void transformAttributeStatement(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers, public void transformAttributeStatement(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers, ResponseType response, KeycloakSession session, UserSessionModel userSession,
ResponseType response, ClientSessionModel clientSession) {
KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) {
AssertionType assertion = response.getAssertions().get(0).getAssertion(); AssertionType assertion = response.getAssertions().get(0).getAssertion();
AttributeStatementType attributeStatement = new AttributeStatementType(); AttributeStatementType attributeStatement = new AttributeStatementType();
@ -432,50 +446,32 @@ public class SamlProtocol implements LoginProtocol {
processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession); processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession);
} }
//SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute // SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
if(attributeStatement.getAttributes().size() > 0) { if (attributeStatement.getAttributes().size() > 0) {
assertion.addStatement(attributeStatement); assertion.addStatement(attributeStatement);
} }
} }
public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers, public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
ResponseType response,
KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) {
for (ProtocolMapperProcessor<SAMLLoginResponseMapper> processor : mappers) { for (ProtocolMapperProcessor<SAMLLoginResponseMapper> processor : mappers) {
response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession); response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession);
} }
return response; return response;
} }
public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper, public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
ResponseType response, if (roleListMapper == null)
KeycloakSession session, return;
UserSessionModel userSession, ClientSessionModel clientSession) {
if (roleListMapper == null) return;
AssertionType assertion = response.getAssertions().get(0).getAssertion(); AssertionType assertion = response.getAssertions().get(0).getAssertion();
AttributeStatementType attributeStatement = new AttributeStatementType(); AttributeStatementType attributeStatement = new AttributeStatementType();
roleListMapper.mapper.mapRoles(attributeStatement, roleListMapper.model, session, userSession, clientSession); roleListMapper.mapper.mapRoles(attributeStatement, roleListMapper.model, session, userSession, clientSession);
//SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute // SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
if(attributeStatement.getAttributes().size() > 0) { if (attributeStatement.getAttributes().size() > 0) {
assertion.addStatement(attributeStatement); assertion.addStatement(attributeStatement);
} }
} }
@Override
public Response consentDenied(ClientSessionModel clientSession) {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
session.sessions().removeClientSession(realm, clientSession);
return ErrorPage.error(session, Messages.CONSENT_DENIED);
} else {
session.sessions().removeClientSession(realm, clientSession);
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
}
}
public static String getLogoutServiceUrl(UriInfo uriInfo, ClientModel client, String bindingType) { public static String getLogoutServiceUrl(UriInfo uriInfo, ClientModel client, String bindingType) {
String logoutServiceUrl = null; String logoutServiceUrl = null;
if (SAML_POST_BINDING.equals(bindingType)) { if (SAML_POST_BINDING.equals(bindingType)) {
@ -483,8 +479,10 @@ public class SamlProtocol implements LoginProtocol {
} else { } else {
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE); logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
} }
if (logoutServiceUrl == null && client instanceof ClientModel) logoutServiceUrl = ((ClientModel)client).getManagementUrl(); if (logoutServiceUrl == null && client instanceof ClientModel)
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) return null; logoutServiceUrl = ((ClientModel) client).getManagementUrl();
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
return null;
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl); return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl);
} }
@ -492,7 +490,8 @@ public class SamlProtocol implements LoginProtocol {
@Override @Override
public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient(); ClientModel client = clientSession.getClient();
if (!(client instanceof ClientModel)) return null; if (!(client instanceof ClientModel))
return null;
try { try {
if (isLogoutPostBindingForClient(clientSession)) { if (isLogoutPostBindingForClient(clientSession)) {
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING); String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
@ -541,9 +540,7 @@ public class SamlProtocol implements LoginProtocol {
if (canonicalization != null) { if (canonicalization != null) {
binding.canonicalizationMethod(canonicalization); binding.canonicalizationMethod(canonicalization);
} }
binding.signatureAlgorithm(algorithm) binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
} }
try { try {
@ -561,8 +558,6 @@ public class SamlProtocol implements LoginProtocol {
} }
} }
@Override @Override
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient(); ClientModel client = clientSession.getClient();
@ -573,7 +568,6 @@ public class SamlProtocol implements LoginProtocol {
} }
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(logoutUrl, clientSession, client); SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(logoutUrl, clientSession, client);
String logoutRequestString = null; String logoutRequestString = null;
try { try {
JaxrsSAML2BindingBuilder binding = createBindingBuilder(client); JaxrsSAML2BindingBuilder binding = createBindingBuilder(client);
@ -583,20 +577,21 @@ public class SamlProtocol implements LoginProtocol {
return; return;
} }
HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient(); HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
for (int i = 0; i < 2; i++) { // follow redirects once for (int i = 0; i < 2; i++) { // follow redirects once
try { try {
List<NameValuePair> formparams = new ArrayList<NameValuePair>(); List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString)); formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink todo remove this formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink
// todo remove
// this
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
HttpPost post = new HttpPost(logoutUrl); HttpPost post = new HttpPost(logoutUrl);
post.setEntity(form); post.setEntity(form);
HttpResponse response = httpClient.execute(post); HttpResponse response = httpClient.execute(post);
try { try {
int status = response.getStatusLine().getStatusCode(); int status = response.getStatusLine().getStatusCode();
if (status == 302 && !logoutUrl.endsWith("/")) { if (status == 302 && !logoutUrl.endsWith("/")) {
String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
String withSlash = logoutUrl + "/"; String withSlash = logoutUrl + "/";
if (withSlash.equals(redirect)) { if (withSlash.equals(redirect)) {
@ -608,7 +603,8 @@ public class SamlProtocol implements LoginProtocol {
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (entity != null) { if (entity != null) {
InputStream is = entity.getContent(); InputStream is = entity.getContent();
if (is != null) is.close(); if (is != null)
is.close();
} }
} }
@ -622,21 +618,15 @@ public class SamlProtocol implements LoginProtocol {
protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, ClientSessionModel clientSession, ClientModel client) { protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, ClientSessionModel clientSession, ClientModel client) {
// build userPrincipal with subject used at login // build userPrincipal with subject used at login
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm)).sessionIndex(clientSession.getId())
.assertionExpiration(realm.getAccessCodeLifespan()) .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)).destination(logoutUrl);
.issuer(getResponseIssuer(realm))
.sessionIndex(clientSession.getId())
.userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT))
.destination(logoutUrl);
return logoutBuilder; return logoutBuilder;
} }
private JaxrsSAML2BindingBuilder createBindingBuilder(ClientModel client) { private JaxrsSAML2BindingBuilder createBindingBuilder(ClientModel client) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(); JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
if (requiresRealmSignature(client)) { if (requiresRealmSignature(client)) {
binding.signatureAlgorithm(getSignatureAlgorithm(client)) binding.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
} }
return binding; return binding;
} }

View file

@ -1,11 +1,23 @@
package org.keycloak.protocol.saml; package org.keycloak.protocol.saml;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.common.util.StreamUtil;
import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
@ -16,15 +28,12 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser; import org.keycloak.saml.SAMLRequestParser;
@ -33,33 +42,10 @@ 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.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.common.util.StreamUtil;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
import java.util.List;
/** /**
* Resource class for the oauth/openid connect token service * Resource class for the oauth/openid connect token service
@ -67,40 +53,12 @@ import java.util.List;
* @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 $
*/ */
public class SamlService { public class SamlService extends AuthorizationEndpointBase {
protected static final Logger logger = Logger.getLogger(SamlService.class); protected static final Logger logger = Logger.getLogger(SamlService.class);
protected RealmModel realm;
private EventBuilder event;
protected AuthenticationManager authManager;
@Context
protected Providers providers;
@Context
protected SecurityContext securityContext;
@Context
protected UriInfo uriInfo;
@Context
protected HttpHeaders headers;
@Context
protected HttpRequest request;
@Context
protected HttpResponse response;
@Context
protected KeycloakSession session;
@Context
protected ClientConnection clientConnection;
/*
@Context
protected ResourceContext resourceContext;
*/
public SamlService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { public SamlService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
this.realm = realm; super(realm, event, authManager);
this.event = event;
this.authManager = authManager;
} }
public abstract class BindingProtocol { public abstract class BindingProtocol {
@ -243,7 +201,7 @@ public class SamlService {
bindingType = SamlProtocol.SAML_POST_BINDING; bindingType = SamlProtocol.SAML_POST_BINDING;
String redirect = null; String redirect = null;
URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes
redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
} else { } else {
if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) { if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
@ -262,7 +220,6 @@ public class SamlService {
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI); return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
} }
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect); clientSession.setRedirectUri(redirect);
@ -286,13 +243,9 @@ public class SamlService {
} }
} }
return newBrowserAuthentication(clientSession); return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive());
} }
private String getBindingType(AuthnRequestType requestAbstractType) { private String getBindingType(AuthnRequestType requestAbstractType) {
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding(); URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
@ -308,10 +261,8 @@ public class SamlService {
} }
private boolean isSupportedNameIdFormat(String nameIdFormat) { private boolean isSupportedNameIdFormat(String nameIdFormat) {
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) || if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())
nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) || || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()) ||
nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
return true; return true;
} }
return false; return false;
@ -340,7 +291,8 @@ public class SamlService {
userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, SamlProtocol.getSignatureAlgorithm(client).toString()); userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, SamlProtocol.getSignatureAlgorithm(client).toString());
} }
if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState); if (relayState != null)
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID()); userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding); userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, client.getAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE)); userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, client.getAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE));
@ -356,7 +308,8 @@ public class SamlService {
} else if (logoutRequest.getSessionIndex() != null) { } else if (logoutRequest.getSessionIndex() != null) {
for (String sessionIndex : logoutRequest.getSessionIndex()) { for (String sessionIndex : logoutRequest.getSessionIndex()) {
ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex); ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex);
if (clientSession == null) continue; if (clientSession == null)
continue;
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
if (clientSession.getClient().getClientId().equals(client.getClientId())) { if (clientSession.getClient().getClientId().equals(client.getClientId())) {
// remove requesting client from logout // remove requesting client from logout
@ -391,13 +344,10 @@ public class SamlService {
builder.logoutRequestID(logoutRequest.getID()); builder.logoutRequestID(logoutRequest.getID());
builder.destination(logoutBindingUri); builder.destination(logoutBindingUri);
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString()); builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder() JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
.relayState(logoutRelayState);
if (SamlProtocol.requiresRealmSignature(client)) { if (SamlProtocol.requiresRealmSignature(client)) {
SignatureAlgorithm algorithm = SamlProtocol.getSignatureAlgorithm(client); SignatureAlgorithm algorithm = SamlProtocol.getSignatureAlgorithm(client);
binding.signatureAlgorithm(algorithm) binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
} }
try { try {
@ -420,7 +370,6 @@ public class SamlService {
} }
} }
protected class PostBindingProtocol extends BindingProtocol { protected class PostBindingProtocol extends BindingProtocol {
@Override @Override
@ -443,12 +392,14 @@ public class SamlService {
return SamlProtocol.SAML_POST_BINDING; return SamlProtocol.SAML_POST_BINDING;
} }
public Response execute(String samlRequest, String samlResponse, String relayState) { public Response execute(String samlRequest, String samlResponse, String relayState) {
Response response = basicChecks(samlRequest, samlResponse); Response response = basicChecks(samlRequest, samlResponse);
if (response != null) return response; if (response != null)
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState); return response;
else return handleSamlResponse(samlResponse, relayState); if (samlRequest != null)
return handleSamlRequest(samlRequest, relayState);
else
return handleSamlResponse(samlResponse, relayState);
} }
} }
@ -464,7 +415,6 @@ public class SamlService {
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY); SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
} }
@Override @Override
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) { protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest); return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
@ -480,74 +430,35 @@ public class SamlService {
return SamlProtocol.SAML_REDIRECT_BINDING; return SamlProtocol.SAML_REDIRECT_BINDING;
} }
public Response execute(String samlRequest, String samlResponse, String relayState) { public Response execute(String samlRequest, String samlResponse, String relayState) {
Response response = basicChecks(samlRequest, samlResponse); Response response = basicChecks(samlRequest, samlResponse);
if (response != null) return response; if (response != null)
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState); return response;
else return handleSamlResponse(samlResponse, relayState); if (samlRequest != null)
return handleSamlRequest(samlRequest, relayState);
else
return handleSamlResponse(samlResponse, relayState);
} }
} }
protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) {
private Response buildRedirectToIdentityProvider(String providerId, String accessCode) { return handleBrowserAuthenticationRequest(clientSession, new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo), isPassive);
logger.debug("Automatically redirect to identity provider: " + providerId);
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(uriInfo.getBaseUri(), providerId, realm.getName(), accessCode))
.build();
} }
protected Response newBrowserAuthentication(ClientSessionModel clientSession) {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
for (IdentityProviderModel identityProvider : identityProviders) {
if (identityProvider.isAuthenticateByDefault()) {
return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode() );
}
}
AuthenticationFlowModel flow = realm.getBrowserFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(flowId)
.setBrowserFlow(true)
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
try {
RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
return processor.authenticate();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
}
/** /**
*/ */
@GET @GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
logger.debug("SAML GET"); logger.debug("SAML GET");
return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState); return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState);
} }
/** /**
*/ */
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState) {
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
logger.debug("SAML POST"); logger.debug("SAML POST");
return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState); return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
} }
@ -570,13 +481,13 @@ public class SamlService {
@GET @GET
@Path("clients/{client}") @Path("clients/{client}")
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, @QueryParam("RelayState") String relayState) {
@QueryParam("RelayState") String relayState) {
event.event(EventType.LOGIN); event.event(EventType.LOGIN);
ClientModel client = null; ClientModel client = null;
for (ClientModel c : realm.getClients()) { for (ClientModel c : realm.getClients()) {
String urlName = c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME); String urlName = c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME);
if (urlName == null) continue; if (urlName == null)
continue;
if (urlName.equals(clientUrlName)) { if (urlName.equals(clientUrlName)) {
client = c; client = c;
break; break;
@ -586,18 +497,14 @@ public class SamlService {
event.error(Errors.CLIENT_NOT_FOUND); event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND); return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND);
} }
if (client.getManagementUrl() == null if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) {
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) {
logger.error("SAML assertion consumer url not set up"); logger.error("SAML assertion consumer url not set up");
event.error(Errors.INVALID_REDIRECT_URI); event.error(Errors.INVALID_REDIRECT_URI);
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI); return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
} }
String bindingType = SamlProtocol.SAML_POST_BINDING; String bindingType = SamlProtocol.SAML_POST_BINDING;
if (client.getManagementUrl() == null if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) {
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) {
bindingType = SamlProtocol.SAML_REDIRECT_BINDING; bindingType = SamlProtocol.SAML_REDIRECT_BINDING;
} }
@ -626,8 +533,7 @@ public class SamlService {
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
} }
return newBrowserAuthentication(clientSession, false);
return newBrowserAuthentication(clientSession);
} }

View file

@ -21,6 +21,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
@ -470,7 +471,7 @@ public class AuthenticationProcessor {
protocol.setRealm(getRealm()) protocol.setRealm(getRealm())
.setHttpHeaders(getHttpRequest().getHttpHeaders()) .setHttpHeaders(getHttpRequest().getHttpHeaders())
.setUriInfo(getUriInfo()); .setUriInfo(getUriInfo());
Response response = protocol.cancelLogin(getClientSession()); Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER);
forceChallenge(response); forceChallenge(response);
} }

View file

@ -0,0 +1,137 @@
package org.keycloak.protocol;
import java.util.List;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.LoginActionsService;
/**
* Common base class for Authorization REST endpoints implementation, which have to be implemented by each protocol.
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
public abstract class AuthorizationEndpointBase {
private static final Logger logger = Logger.getLogger(AuthorizationEndpointBase.class);
protected RealmModel realm;
protected EventBuilder event;
protected AuthenticationManager authManager;
@Context
protected UriInfo uriInfo;
@Context
protected HttpHeaders headers;
@Context
protected HttpRequest request;
@Context
protected KeycloakSession session;
@Context
protected ClientConnection clientConnection;
public AuthorizationEndpointBase(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
this.realm = realm;
this.event = event;
this.authManager = authManager;
}
protected AuthenticationProcessor createProcessor(ClientSessionModel clientSession, String flowId, String flowPath) {
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowPath(flowPath)
.setFlowId(flowId)
.setBrowserFlow(true)
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
return processor;
}
/**
* Common method to handle browser authentication request in protocols unified way.
*
* @param clientSession for current request
* @param protocol handler for protocol used to initiate login
* @param isPassive set to true if login should be passive (without login screen shown)
* @return response to be returned to the browser
*/
protected Response handleBrowserAuthenticationRequest(ClientSessionModel clientSession, LoginProtocol protocol, boolean isPassive) {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
for (IdentityProviderModel identityProvider : identityProviders) {
if (identityProvider.isAuthenticateByDefault()) {
// TODO if we are isPassive we should propagate this flag to default identity provider also if possible
return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode());
}
}
AuthenticationFlowModel flow = realm.getBrowserFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
if (isPassive) {
// OIDC prompt == NONE or SAML 2 IsPassive flag
// This means that client is just checking if the user is already completely logged in.
// We cancel login if any authentication action or required action is required
Response challenge = null;
Response challenge2 = null;
try {
challenge = processor.authenticateOnly();
if (challenge == null) {
challenge2 = processor.attachSessionExecutionRequiredActions();
}
} catch (Exception e) {
return processor.handleBrowserException(e);
}
if (challenge != null || challenge2 != null) {
if (processor.isUserSessionCreated()) {
session.sessions().removeUserSession(realm, processor.getUserSession());
}
if (challenge != null)
return protocol.sendError(clientSession, Error.PASSIVE_LOGIN_REQUIRED);
else
return protocol.sendError(clientSession, Error.PASSIVE_INTERACTION_REQUIRED);
} else {
return processor.finishAuthentication();
}
} else {
try {
RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
return processor.authenticate();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
}
}
protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
logger.debug("Automatically redirect to identity provider: " + providerId);
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode))
.build();
}
}

View file

@ -17,6 +17,28 @@ import javax.ws.rs.core.UriInfo;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface LoginProtocol extends Provider { public interface LoginProtocol extends Provider {
public static enum Error {
/**
* Login cancelled by the user
*/
CANCELLED_BY_USER,
/**
* Consent denied by the user
*/
CONSENT_DENIED,
/**
* Passive authentication mode requested but nobody is logged in
*/
PASSIVE_LOGIN_REQUIRED,
/**
* Passive authentication mode requested, user is logged in, but some other user interaction is necessary (eg. some required login actions exist or Consent approval is necessary for logged in
* user)
*/
PASSIVE_INTERACTION_REQUIRED;
}
LoginProtocol setSession(KeycloakSession session); LoginProtocol setSession(KeycloakSession session);
LoginProtocol setRealm(RealmModel realm); LoginProtocol setRealm(RealmModel realm);
@ -27,11 +49,12 @@ public interface LoginProtocol extends Provider {
LoginProtocol setEventBuilder(EventBuilder event); LoginProtocol setEventBuilder(EventBuilder event);
Response cancelLogin(ClientSessionModel clientSession);
Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode); Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode);
Response consentDenied(ClientSessionModel clientSession);
Response sendError(ClientSessionModel clientSession, Error error);
void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession); void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession); Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
Response finishLogout(UserSessionModel userSession); Response finishLogout(UserSessionModel userSession);
} }

View file

@ -82,7 +82,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
this.event = event; this.event = event;
} }
public OIDCLoginProtocol(){ public OIDCLoginProtocol() {
} }
@ -105,7 +105,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
} }
@Override @Override
public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers){ public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers) {
this.headers = headers; this.headers = headers;
return this; return this;
} }
@ -116,19 +116,6 @@ public class OIDCLoginProtocol implements LoginProtocol {
return this; return this;
} }
@Override
public Response cancelLogin(ClientSessionModel clientSession) {
String redirect = clientSession.getRedirectUri();
String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied");
if (state != null) {
redirectUri.queryParam(OAuth2Constants.STATE, state);
}
session.sessions().removeClientSession(realm, clientSession);
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
return Response.status(302).location(redirectUri.build()).build();
}
@Override @Override
public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
ClientSessionModel clientSession = accessCode.getClientSession(); ClientSessionModel clientSession = accessCode.getClientSession();
@ -144,10 +131,11 @@ public class OIDCLoginProtocol implements LoginProtocol {
return location.build(); return location.build();
} }
public Response consentDenied(ClientSessionModel clientSession) { @Override
public Response sendError(ClientSessionModel clientSession, Error error) {
String redirect = clientSession.getRedirectUri(); String redirect = clientSession.getRedirectUri();
String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM); String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied"); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, translateError(error));
if (state != null) if (state != null)
redirectUri.queryParam(OAuth2Constants.STATE, state); redirectUri.queryParam(OAuth2Constants.STATE, state);
session.sessions().removeClientSession(realm, clientSession); session.sessions().removeClientSession(realm, clientSession);
@ -156,20 +144,25 @@ public class OIDCLoginProtocol implements LoginProtocol {
return location.build(); return location.build();
} }
private String translateError(Error error) {
public Response invalidSessionError(ClientSessionModel clientSession) { switch (error) {
String redirect = clientSession.getRedirectUri(); case CANCELLED_BY_USER:
String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM); case CONSENT_DENIED:
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied"); return "access_denied";
if (state != null) { case PASSIVE_INTERACTION_REQUIRED:
redirectUri.queryParam(OAuth2Constants.STATE, state); return "interaction_required";
case PASSIVE_LOGIN_REQUIRED:
return "login_required";
default:
log.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
return "access_denied";
} }
return Response.status(302).location(redirectUri.build()).build();
} }
@Override @Override
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
if (!(clientSession.getClient() instanceof ClientModel)) return; if (!(clientSession.getClient() instanceof ClientModel))
return;
ClientModel app = clientSession.getClient(); ClientModel app = clientSession.getClient();
new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession); new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession);
} }
@ -190,10 +183,10 @@ public class OIDCLoginProtocol implements LoginProtocol {
} }
event.user(userSession.getUser()).session(userSession).success(); event.user(userSession.getUser()).session(userSession).success();
if (redirectUri != null) { if (redirectUri != null) {
UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri); UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri);
if (state != null) uriBuilder.queryParam(STATE_PARAM, state); if (state != null)
uriBuilder.queryParam(STATE_PARAM, state);
return Response.status(302).location(uriBuilder.build()).build(); return Response.status(302).location(uriBuilder.build()).build();
} else { } else {
return Response.ok().build(); return Response.ok().build();

View file

@ -1,10 +1,19 @@
package org.keycloak.protocol.oidc.endpoints; package org.keycloak.protocol.oidc.endpoints;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.common.ClientConnection;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@ -18,6 +27,7 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.protocol.oidc.utils.RedirectUtils;
@ -28,45 +38,19 @@ import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.LoginActionsService;
import javax.ws.rs.GET;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class AuthorizationEndpoint { public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class); private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class);
public static final String CODE_AUTH_TYPE = "code"; public static final String CODE_AUTH_TYPE = "code";
private enum Action { private enum Action {
REGISTER, CODE, FORGOT_CREDENTIALS REGISTER, CODE, FORGOT_CREDENTIALS
} }
@Context
private KeycloakSession session;
@Context
private HttpRequest request;
@Context
private HttpHeaders headers;
@Context
private UriInfo uriInfo;
@Context
private ClientConnection clientConnection;
private final AuthenticationManager authManager;
private final RealmModel realm;
private final EventBuilder event;
private ClientModel client; private ClientModel client;
private ClientSessionModel clientSession; private ClientSessionModel clientSession;
@ -86,9 +70,7 @@ public class AuthorizationEndpoint {
private String legacyResponseType; private String legacyResponseType;
public AuthorizationEndpoint(AuthenticationManager authManager, RealmModel realm, EventBuilder event) { public AuthorizationEndpoint(AuthenticationManager authManager, RealmModel realm, EventBuilder event) {
this.authManager = authManager; super(realm, event, authManager);
this.realm = realm;
this.event = event;
event.event(EventType.LOGIN); event.event(EventType.LOGIN);
} }
@ -249,7 +231,6 @@ public class AuthorizationEndpoint {
} }
private Response buildAuthorizationCodeAuthorizationResponse() { private Response buildAuthorizationCodeAuthorizationResponse() {
String accessCode = new ClientSessionCode(realm, clientSession).getCode();
if (idpHint != null && !"".equals(idpHint)) { if (idpHint != null && !"".equals(idpHint)) {
IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint); IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint);
@ -259,65 +240,13 @@ public class AuthorizationEndpoint {
.setError(Messages.IDENTITY_PROVIDER_NOT_FOUND, idpHint) .setError(Messages.IDENTITY_PROVIDER_NOT_FOUND, idpHint)
.createErrorPage(); .createErrorPage();
} }
return buildRedirectToIdentityProvider(idpHint, accessCode); return buildRedirectToIdentityProvider(idpHint, new ClientSessionCode(realm, clientSession).getCode());
} }
return browserAuthentication(accessCode);
}
protected Response browserAuthentication(String accessCode) {
this.event.event(EventType.LOGIN); this.event.event(EventType.LOGIN);
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
for (IdentityProviderModel identityProvider : identityProviders) {
if (identityProvider.isAuthenticateByDefault()) {
return buildRedirectToIdentityProvider(identityProvider.getAlias(), accessCode);
}
}
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), prompt != null && prompt.equals("none"));
AuthenticationFlowModel flow = realm.getBrowserFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.AUTHENTICATE_PATH);
if (prompt != null && prompt.equals("none")) {
// OIDC prompt == NONE
// This means that client is just checking if the user is already completely logged in.
//
// here we cancel login if any authentication action or required action is required
Response challenge = null;
try {
challenge = processor.authenticateOnly();
if (challenge == null) {
challenge = processor.attachSessionExecutionRequiredActions();
}
} catch (Exception e) {
return processor.handleBrowserException(e);
}
if (challenge != null) {
if (processor.isUserSessionCreated()) {
session.sessions().removeUserSession(realm, processor.getUserSession());
}
OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers, event);
return oauth.cancelLogin(clientSession);
}
if (challenge == null) {
return processor.finishAuthentication();
} else {
RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
return challenge;
}
} else {
try {
RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
return processor.authenticate();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
}
} }
private Response buildRegister() { private Response buildRegister() {
@ -326,7 +255,7 @@ public class AuthorizationEndpoint {
AuthenticationFlowModel flow = realm.getRegistrationFlow(); AuthenticationFlowModel flow = realm.getRegistrationFlow();
String flowId = flow.getId(); String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.REGISTRATION_PATH); AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.REGISTRATION_PATH);
return processor.authenticate(); return processor.authenticate();
} }
@ -337,32 +266,12 @@ public class AuthorizationEndpoint {
AuthenticationFlowModel flow = realm.getResetCredentialsFlow(); AuthenticationFlowModel flow = realm.getResetCredentialsFlow();
String flowId = flow.getId(); String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.RESET_CREDENTIALS_PATH); AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
return processor.authenticate(); return processor.authenticate();
} }
private AuthenticationProcessor createProcessor(String flowId, String flowPath) {
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowPath(flowPath)
.setFlowId(flowId)
.setBrowserFlow(true)
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
return processor;
}
private Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
logger.debug("Automatically redirect to identity provider: " + providerId);
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode))
.build();
}
} }

View file

@ -36,6 +36,7 @@ import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -523,7 +524,7 @@ public class AuthenticationManager {
.setHttpHeaders(context.getHttpRequest().getHttpHeaders()) .setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo()); .setUriInfo(context.getUriInfo());
event.error(Errors.REJECTED_BY_USER); event.error(Errors.REJECTED_BY_USER);
return protocol.consentDenied(context.getClientSession()); return protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
} }
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId()); clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId());

View file

@ -57,6 +57,7 @@ import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
@ -591,7 +592,7 @@ public class LoginActionsService {
.setHttpHeaders(headers) .setHttpHeaders(headers)
.setUriInfo(uriInfo); .setUriInfo(uriInfo);
event.error(Errors.REJECTED_BY_USER); event.error(Errors.REJECTED_BY_USER);
return protocol.consentDenied(clientSession); return protocol.sendError(clientSession, Error.CONSENT_DENIED);
} }
UserConsentModel grantedConsent = user.getConsentByClient(client.getId()); UserConsentModel grantedConsent = user.getConsentByClient(client.getId());
@ -828,7 +829,7 @@ public class LoginActionsService {
.setHttpHeaders(context.getHttpRequest().getHttpHeaders()) .setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo()); .setUriInfo(context.getUriInfo());
event.detail(Details.CUSTOM_REQUIRED_ACTION, action).error(Errors.REJECTED_BY_USER); event.detail(Details.CUSTOM_REQUIRED_ACTION, action).error(Errors.REJECTED_BY_USER);
return protocol.consentDenied(context.getClientSession()); return protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
} }
throw new RuntimeException("Unreachable"); throw new RuntimeException("Unreachable");

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,38 @@ 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.
// Shown page depends on used authentication mechanism, some may return forbidden error, some return requested page with anonymous user (not logged in)
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("HTTP status code: 403"));
} 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

@ -6,7 +6,6 @@ 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.servlet.http.HttpSession;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
@ -20,10 +19,17 @@ public class ErrorServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
authError = (AuthenticationError)req.getAttribute(AuthenticationError.class.getName()); authError = (AuthenticationError)req.getAttribute(AuthenticationError.class.getName());
Integer statusCode = (Integer) req.getAttribute("javax.servlet.error.status_code");
resp.setContentType("text/html"); resp.setContentType("text/html");
PrintWriter pw = resp.getWriter(); PrintWriter pw = resp.getWriter();
pw.printf("<html><head><title>%s</title></head><body>", "Error Page"); pw.printf("<html><head><title>%s</title></head><body>", "Error Page");
pw.print("<h1>There was an error</h1></body></html>"); pw.print("<h1>There was an error</h1>");
if (statusCode != null)
pw.print("<br/>HTTP status code: " + statusCode);
if (authError != null)
pw.print("<br/>Error info: " + authError.toString());
pw.print("</body></html>");
pw.flush(); pw.flush();

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,