Merge pull request #1840 from velias/KEYCLOAK-2075
KEYCLOAK-2075 KEYCLOAK-2107 - support for SAML IsPassive mode
This commit is contained in:
commit
d6e2bccb16
38 changed files with 685 additions and 567 deletions
|
@ -11,7 +11,8 @@
|
|||
sslPolicy="EXTERNAL"
|
||||
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||
logoutPage="/logout.jsp"
|
||||
forceAuthentication="false">
|
||||
forceAuthentication="false"
|
||||
isPassive="false">
|
||||
<Keys>
|
||||
<Key signing="true" >
|
||||
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
|
||||
|
@ -63,7 +64,8 @@
|
|||
<SP entityID="sp"
|
||||
sslPolicy="ssl"
|
||||
nameIDPolicyFormat="format"
|
||||
forceAuthentication="true">
|
||||
forceAuthentication="true"
|
||||
isPassive="false">
|
||||
...
|
||||
</SP>]]></programlisting>
|
||||
<para>
|
||||
|
@ -106,12 +108,23 @@
|
|||
<listitem>
|
||||
<para>
|
||||
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.
|
||||
<emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
|
||||
</para>
|
||||
</listitem>
|
||||
</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>
|
||||
</para>
|
||||
</section>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<chapter id="tomcat-adapter">
|
||||
<title>Tomcat 6, 7 and 8 SAML dapters</title>
|
||||
<title>Tomcat 6, 7 and 8 SAML adapters</title>
|
||||
<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
|
||||
into your Tomcat installation. You then have to provide some extra configuration in each WAR you deploy to
|
||||
|
|
|
@ -36,4 +36,11 @@ public class OIDCAuthenticationError implements AuthenticationError {
|
|||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OIDCAuthenticationError [reason=" + reason + ", description=" + description + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -5,8 +5,5 @@ package org.keycloak.adapters.spi;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public enum AuthOutcome {
|
||||
NOT_ATTEMPTED,
|
||||
FAILED,
|
||||
AUTHENTICATED,
|
||||
LOGGED_OUT
|
||||
NOT_ATTEMPTED, FAILED, AUTHENTICATED, NOT_AUTHENTICATED, LOGGED_OUT
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package org.keycloak.adapters.saml;
|
||||
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
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>
|
||||
* @version $Revision: 1 $
|
||||
|
@ -31,7 +31,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
return validateResponseSignature;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public Binding getRequestBinding() {
|
||||
return requestBinding;
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
return signResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public Binding getRequestBinding() {
|
||||
return requestBinding;
|
||||
}
|
||||
|
@ -150,12 +150,8 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class DefaultIDP implements IDP {
|
||||
|
||||
|
||||
|
||||
private String entityID;
|
||||
private PublicKey signatureValidationKey;
|
||||
private SingleSignOnService singleSignOnService;
|
||||
|
@ -204,6 +200,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
private String entityID;
|
||||
private String nameIDPolicyFormat;
|
||||
private boolean forceAuthentication;
|
||||
private boolean isPassive;
|
||||
private PrivateKey decryptionKey;
|
||||
private KeyPair signingKeyPair;
|
||||
private String assertionConsumerServiceUrl;
|
||||
|
@ -214,7 +211,6 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
private SignatureAlgorithm signatureAlgorithm;
|
||||
private String signatureCanonicalizationMethod;
|
||||
|
||||
|
||||
@Override
|
||||
public IDP getIDP() {
|
||||
return idp;
|
||||
|
@ -245,6 +241,11 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
return forceAuthentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIsPassive() {
|
||||
return isPassive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey getDecryptionKey() {
|
||||
return decryptionKey;
|
||||
|
@ -265,7 +266,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
return roleAttributeNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public PrincipalNamePolicy getPrincipalNamePolicy() {
|
||||
return principalNamePolicy;
|
||||
}
|
||||
|
@ -299,6 +300,10 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
this.forceAuthentication = forceAuthentication;
|
||||
}
|
||||
|
||||
public void setIsPassive(boolean isPassive){
|
||||
this.isPassive = isPassive;
|
||||
}
|
||||
|
||||
public void setDecryptionKey(PrivateKey decryptionKey) {
|
||||
this.decryptionKey = decryptionKey;
|
||||
}
|
||||
|
@ -332,7 +337,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
|||
this.logoutPage = logoutPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public String getSignatureCanonicalizationMethod() {
|
||||
return signatureCanonicalizationMethod;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ public class InitiateLogin implements AuthChallenge {
|
|||
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
||||
.destination(destinationUrl)
|
||||
.issuer(issuerURL)
|
||||
.forceAuthn(deployment.isForceAuthentication())
|
||||
.forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive())
|
||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
||||
if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) {
|
||||
String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();
|
||||
|
|
|
@ -40,4 +40,10 @@ public class SamlAuthenticationError implements AuthenticationError {
|
|||
public StatusResponseType getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SamlAuthenticationError [reason=" + reason + ", status=" + status + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.RequestAbstractType;
|
||||
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.StatusType;
|
||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||
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.exceptions.ProcessingException;
|
||||
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.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
||||
|
@ -207,6 +210,7 @@ public abstract class SamlAuthenticator {
|
|||
log.error("Request URI does not match SAML request destination");
|
||||
return AuthOutcome.FAILED;
|
||||
}
|
||||
|
||||
if (statusResponse instanceof ResponseType) {
|
||||
try {
|
||||
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
|
||||
|
@ -254,7 +258,15 @@ public abstract class SamlAuthenticator {
|
|||
}
|
||||
|
||||
} else if (sessionStore.isLoggingIn()) {
|
||||
|
||||
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() {
|
||||
@Override
|
||||
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) {
|
||||
|
||||
AssertionType assertion = null;
|
||||
try {
|
||||
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
|
||||
|
@ -295,7 +316,7 @@ public abstract class SamlAuthenticator {
|
|||
return initiateLogin();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error extracting SAML assertion, e");
|
||||
log.error("Error extracting SAML assertion: " + e.getMessage());
|
||||
challenge = new AuthChallenge() {
|
||||
@Override
|
||||
public boolean challenge(HttpFacade exchange) {
|
||||
|
@ -434,9 +455,9 @@ public abstract class SamlAuthenticator {
|
|||
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
||||
}
|
||||
|
||||
|
||||
protected SAMLDocumentHolder extractPostBindingResponse(String response) {
|
||||
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
||||
String xml = new String(samlBytes);
|
||||
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ public interface SamlDeployment {
|
|||
String getEntityID();
|
||||
String getNameIDPolicyFormat();
|
||||
boolean isForceAuthentication();
|
||||
boolean isIsPassive();
|
||||
PrivateKey getDecryptionKey();
|
||||
KeyPair getSigningKeyPair();
|
||||
String getSignatureCanonicalizationMethod();
|
||||
|
|
|
@ -33,6 +33,7 @@ public class SP implements Serializable {
|
|||
private String entityID;
|
||||
private String sslPolicy;
|
||||
private boolean forceAuthentication;
|
||||
private boolean isPassive;
|
||||
private String logoutPage;
|
||||
private List<Key> keys;
|
||||
private String nameIDPolicyFormat;
|
||||
|
@ -64,6 +65,14 @@ public class SP implements Serializable {
|
|||
this.forceAuthentication = forceAuthentication;
|
||||
}
|
||||
|
||||
public boolean isIsPassive() {
|
||||
return isPassive;
|
||||
}
|
||||
|
||||
public void setIsPassive(boolean isPassive) {
|
||||
this.isPassive = isPassive;
|
||||
}
|
||||
|
||||
public List<Key> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
|
|
@ -5,17 +5,17 @@ package org.keycloak.adapters.saml.config.parsers;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ConfigXmlConstants {
|
||||
public static final String KEYCLOAK_SAML_ADAPTER ="keycloak-saml-adapter";
|
||||
public static final String SP_ELEMENT="SP";
|
||||
public static final String KEYCLOAK_SAML_ADAPTER = "keycloak-saml-adapter";
|
||||
public static final String SP_ELEMENT = "SP";
|
||||
public static final String ENTITY_ID_ATTR = "entityID";
|
||||
public static final String SSL_POLICY_ATTR = "sslPolicy";
|
||||
public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat";
|
||||
public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication";
|
||||
public static final String IS_PASSIVE_ATTR = "isPassive";
|
||||
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
|
||||
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
|
||||
public static final String LOGOUT_PAGE_ATTR = "logoutPage";
|
||||
|
||||
|
||||
public static final String KEYS_ELEMENT = "Keys";
|
||||
public static final String KEY_ELEMENT = "Key";
|
||||
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 ATTRIBUTE_ATTR = "attribute";
|
||||
|
||||
|
||||
public static final String ROLE_IDENTIFIERS_ELEMENT = "RoleIdentifiers";
|
||||
public static final String ATTRIBUTE_ELEMENT = "Attribute";
|
||||
public static final String NAME_ATTR = "name";
|
||||
|
|
|
@ -41,6 +41,7 @@ public class DeploymentBuilder {
|
|||
deployment.setConfigured(true);
|
||||
deployment.setEntityID(sp.getEntityID());
|
||||
deployment.setForceAuthentication(sp.isForceAuthentication());
|
||||
deployment.setIsPassive(sp.isIsPassive());
|
||||
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
|
||||
deployment.setLogoutPage(sp.getLogoutPage());
|
||||
deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
package org.keycloak.adapters.saml.config.parsers;
|
||||
|
||||
import org.keycloak.adapters.saml.config.IDP;
|
||||
import org.keycloak.adapters.saml.config.Key;
|
||||
import org.keycloak.adapters.saml.config.SP;
|
||||
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 java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
import javax.xml.stream.events.EndElement;
|
||||
import javax.xml.stream.events.StartElement;
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.adapters.saml.config.IDP;
|
||||
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>
|
||||
|
@ -25,13 +26,16 @@ public class SPXmlParser extends AbstractParser {
|
|||
|
||||
public static String getAttributeValue(StartElement startElement, String tag) {
|
||||
String str = StaxParserUtil.getAttributeValue(startElement, tag);
|
||||
if (str != null) return StringPropertyReplacer.replaceProperties(str);
|
||||
else return str;
|
||||
if (str != null)
|
||||
return StringPropertyReplacer.replaceProperties(str);
|
||||
else
|
||||
return str;
|
||||
}
|
||||
|
||||
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
|
||||
String result = getAttributeValue(startElement, tag);
|
||||
if (result == null) return defaultValue;
|
||||
if (result == null)
|
||||
return defaultValue;
|
||||
return Boolean.valueOf(result);
|
||||
}
|
||||
|
||||
|
@ -41,11 +45,11 @@ public class SPXmlParser extends AbstractParser {
|
|||
|
||||
public static String getElementText(XMLEventReader xmlEventReader) throws ParsingException {
|
||||
String result = StaxParserUtil.getElementText(xmlEventReader);
|
||||
if (result != null) result = StringPropertyReplacer.replaceProperties(result);
|
||||
if (result != null)
|
||||
result = StringPropertyReplacer.replaceProperties(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object parse(XMLEventReader xmlEventReader) throws ParsingException {
|
||||
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||
|
@ -61,6 +65,7 @@ public class SPXmlParser extends AbstractParser {
|
|||
sp.setLogoutPage(getAttributeValue(startElement, ConfigXmlConstants.LOGOUT_PAGE_ATTR));
|
||||
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
|
||||
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
|
||||
sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
|
||||
while (xmlEventReader.hasNext()) {
|
||||
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||
if (xmlEvent == null)
|
||||
|
@ -79,7 +84,7 @@ public class SPXmlParser extends AbstractParser {
|
|||
String tag = StaxParserUtil.getStartElementName(startElement);
|
||||
if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
|
||||
KeysXmlParser parser = new KeysXmlParser();
|
||||
List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
|
||||
List<Key> keys = (List<Key>) parser.parse(xmlEventReader);
|
||||
sp.setKeys(keys);
|
||||
} else if (tag.equals(ConfigXmlConstants.PRINCIPAL_NAME_MAPPING_ELEMENT)) {
|
||||
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||
|
@ -98,7 +103,7 @@ public class SPXmlParser extends AbstractParser {
|
|||
parseRoleMapping(xmlEventReader, sp);
|
||||
} else if (tag.equals(ConfigXmlConstants.IDP_ELEMENT)) {
|
||||
IDPXmlParser parser = new IDPXmlParser();
|
||||
IDP idp = (IDP)parser.parse(xmlEventReader);
|
||||
IDP idp = (IDP) parser.parse(xmlEventReader);
|
||||
sp.setIdp(idp);
|
||||
} else {
|
||||
StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
|
||||
|
@ -108,7 +113,7 @@ public class SPXmlParser extends AbstractParser {
|
|||
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);
|
||||
StaxParserUtil.validate(startElement, ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT);
|
||||
Set<String> roleAttributes = new HashSet<>();
|
||||
|
@ -144,7 +149,6 @@ public class SPXmlParser extends AbstractParser {
|
|||
sp.setRoleAttributes(roleAttributes);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supports(QName qname) {
|
||||
return false;
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
<xs:attribute name="nameIDPolicyFormat" 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="isPassive" type="xs:boolean" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="keys-type">
|
||||
|
|
|
@ -68,6 +68,7 @@ public class XmlParserTest {
|
|||
Assert.assertEquals("ssl", sp.getSslPolicy());
|
||||
Assert.assertEquals("format", sp.getNameIDPolicyFormat());
|
||||
Assert.assertTrue(sp.isForceAuthentication());
|
||||
Assert.assertTrue(sp.isIsPassive());
|
||||
Assert.assertEquals(2, sp.getKeys().size());
|
||||
Key signing = sp.getKeys().get(0);
|
||||
Assert.assertTrue(signing.isSigning());
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<SP entityID="sp"
|
||||
sslPolicy="ssl"
|
||||
nameIDPolicyFormat="format"
|
||||
forceAuthentication="true">
|
||||
forceAuthentication="true"
|
||||
isPassive="true">
|
||||
<Keys>
|
||||
<Key signing="true" >
|
||||
<KeyStore file="file" resource="cp" password="pw">
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
nameIDPolicyFormat="format"
|
||||
signatureAlgorithm=""
|
||||
signatureCanonicalizationMethod=""
|
||||
forceAuthentication="true">
|
||||
forceAuthentication="true"
|
||||
isPassive="true">
|
||||
<Keys>
|
||||
<Key signing="true" >
|
||||
<KeyStore file="file" resource="cp" password="pw">
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
package org.keycloak.adapters.saml.servlet;
|
||||
|
||||
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.adapters.saml.DefaultSamlDeployment;
|
||||
import org.keycloak.adapters.saml.SamlAuthenticator;
|
||||
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 java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
|
@ -24,12 +17,20 @@ import javax.servlet.ServletResponse;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.keycloak.adapters.saml.DefaultSamlDeployment;
|
||||
import org.keycloak.adapters.saml.SamlAuthenticator;
|
||||
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.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>
|
||||
|
@ -38,7 +39,7 @@ import java.util.logging.Logger;
|
|||
public class SamlFilter implements Filter {
|
||||
protected SamlDeploymentContext deploymentContext;
|
||||
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||
private final static Logger log = Logger.getLogger(""+SamlFilter.class);
|
||||
private final static Logger log = Logger.getLogger("" + SamlFilter.class);
|
||||
|
||||
@Override
|
||||
public void init(final FilterConfig filterConfig) throws ServletException {
|
||||
|
@ -46,12 +47,14 @@ public class SamlFilter implements Filter {
|
|||
if (configResolverClass != null) {
|
||||
try {
|
||||
throw new RuntimeException("Not implemented yet");
|
||||
//KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
|
||||
//deploymentContext = new SamlDeploymentContext(configResolver);
|
||||
//log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
|
||||
// KeycloakConfigResolver configResolver = (KeycloakConfigResolver)
|
||||
// context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
|
||||
// deploymentContext = new SamlDeploymentContext(configResolver);
|
||||
// log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.",
|
||||
// configResolverClass);
|
||||
} 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()});
|
||||
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
||||
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());
|
||||
}
|
||||
} else {
|
||||
String fp = filterConfig.getInitParameter("keycloak.config.file");
|
||||
|
@ -65,7 +68,8 @@ public class SamlFilter implements Filter {
|
|||
} else {
|
||||
String path = "/WEB-INF/keycloak-saml.xml";
|
||||
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
|
||||
if (pathParam != null) path = pathParam;
|
||||
if (pathParam != null)
|
||||
path = pathParam;
|
||||
is = filterConfig.getServletContext().getResourceAsStream(path);
|
||||
}
|
||||
final SamlDeployment deployment;
|
||||
|
@ -105,7 +109,6 @@ public class SamlFilter implements Filter {
|
|||
}
|
||||
FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper);
|
||||
|
||||
|
||||
SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) {
|
||||
@Override
|
||||
protected void completeAuthentication(SamlSession account) {
|
||||
|
@ -139,6 +142,16 @@ public class SamlFilter implements Filter {
|
|||
challenge.challenge(facade);
|
||||
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()) {
|
||||
response.sendError(403);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,15 @@
|
|||
*/
|
||||
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.NotificationReceiver;
|
||||
import io.undertow.security.api.SecurityContext;
|
||||
|
@ -24,14 +33,6 @@ import io.undertow.server.HttpServerExchange;
|
|||
import io.undertow.util.AttachmentKey;
|
||||
import io.undertow.util.Headers;
|
||||
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.
|
||||
|
@ -44,8 +45,7 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
|
|||
protected UndertowUserSessionManagement sessionManagement;
|
||||
protected String errorPage;
|
||||
|
||||
public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement,
|
||||
String errorPage) {
|
||||
public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) {
|
||||
this.deploymentContext = deploymentContext;
|
||||
this.sessionManagement = sessionManagement;
|
||||
this.errorPage = errorPage;
|
||||
|
@ -69,19 +69,19 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
|
|||
}
|
||||
|
||||
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;
|
||||
exchange.getResponseHeaders().put(Headers.LOCATION, loc);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected void registerNotifications(final SecurityContext securityContext) {
|
||||
|
||||
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
|
||||
@Override
|
||||
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();
|
||||
UndertowHttpFacade facade = createFacade(exchange);
|
||||
|
@ -104,13 +104,16 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
|
|||
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
|
||||
}
|
||||
SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext);
|
||||
UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade,
|
||||
deploymentContext.resolveDeployment(facade), sessionStore);
|
||||
UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade, deploymentContext.resolveDeployment(facade), sessionStore);
|
||||
AuthOutcome outcome = authenticator.authenticate();
|
||||
if (outcome == AuthOutcome.AUTHENTICATED) {
|
||||
registerNotifications(securityContext);
|
||||
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) {
|
||||
securityContext.logout();
|
||||
if (deployment.getLogoutPage() != null) {
|
||||
|
|
|
@ -80,4 +80,10 @@ public class StatusCodeType implements Serializable {
|
|||
public void setValue(URI value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StatusCodeType [value=" + value + ", statusCode=" + statusCode + "]";
|
||||
}
|
||||
|
||||
}
|
|
@ -100,4 +100,9 @@ public class StatusType implements Serializable {
|
|||
this.statusDetail = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StatusType [statusCode=" + statusCode + ", statusMessage=" + statusMessage + ", statusDetail=" + statusDetail + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,16 +17,16 @@
|
|||
*/
|
||||
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.processing.api.saml.v2.request.SAML2Request;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||
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 java.net.URI;
|
||||
|
||||
/**
|
||||
* @author pedroigor
|
||||
*/
|
||||
|
@ -64,6 +64,11 @@ public class SAML2AuthnRequestBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public SAML2AuthnRequestBuilder isPassive(boolean isPassive) {
|
||||
this.authnRequestType.setIsPassive(isPassive);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) {
|
||||
this.authnRequestType.setNameIDPolicy(nameIDPolicy.build());
|
||||
return this;
|
||||
|
@ -74,7 +79,7 @@ public class SAML2AuthnRequestBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Document toDocument() {
|
||||
public Document toDocument() {
|
||||
try {
|
||||
AuthnRequestType authnRequestType = this.authnRequestType;
|
||||
|
||||
|
|
|
@ -1,5 +1,22 @@
|
|||
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.HttpResponse;
|
||||
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.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
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>
|
||||
* @version $Revision: 1 $
|
||||
|
@ -67,7 +68,6 @@ import java.util.UUID;
|
|||
public class SamlProtocol implements LoginProtocol {
|
||||
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
|
||||
|
||||
|
||||
public static final String ATTRIBUTE_TRUE_VALUE = "true";
|
||||
public static final String ATTRIBUTE_FALSE_VALUE = "false";
|
||||
public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
|
||||
|
@ -115,7 +115,6 @@ public class SamlProtocol implements LoginProtocol {
|
|||
|
||||
protected EventBuilder event;
|
||||
|
||||
|
||||
@Override
|
||||
public SamlProtocol setSession(KeycloakSession session) {
|
||||
this.session = session;
|
||||
|
@ -135,7 +134,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public SamlProtocol setHttpHeaders(HttpHeaders headers){
|
||||
public SamlProtocol setHttpHeaders(HttpHeaders headers) {
|
||||
this.headers = headers;
|
||||
return this;
|
||||
}
|
||||
|
@ -146,22 +145,65 @@ public class SamlProtocol implements LoginProtocol {
|
|||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response cancelLogin(ClientSessionModel clientSession) {
|
||||
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
|
||||
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
|
||||
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("realm", realm.getName());
|
||||
params.put("protocol", LOGIN_PROTOCOL);
|
||||
params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
|
||||
public Response sendError(ClientSessionModel clientSession, Error error) {
|
||||
try {
|
||||
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
|
||||
if (error == Error.CANCELLED_BY_USER) {
|
||||
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("realm", realm.getName());
|
||||
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);
|
||||
URI redirect = builder.buildFromMap(params);
|
||||
return Response.status(302).location(redirect).build();
|
||||
} else {
|
||||
session.sessions().removeClientSession(realm, clientSession);
|
||||
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
|
||||
}
|
||||
}
|
||||
|
||||
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
|
||||
|
@ -207,7 +228,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
|
||||
if (logoutPostUrl == null) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -218,11 +240,13 @@ public class SamlProtocol implements LoginProtocol {
|
|||
String bindingType = clientSession.getNote(SAML_BINDING);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
if(nameIdFormat == null) return SAML_DEFAULT_NAMEID_FORMAT;
|
||||
if (nameIdFormat == null)
|
||||
return SAML_DEFAULT_NAMEID_FORMAT;
|
||||
return nameIdFormat;
|
||||
}
|
||||
|
||||
|
@ -259,20 +284,21 @@ public class SamlProtocol implements LoginProtocol {
|
|||
protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||
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.
|
||||
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.
|
||||
UserModel user = userSession.getUser();
|
||||
String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId();
|
||||
String samlPersistentId = user.getFirstAttribute(name);
|
||||
if (samlPersistentId != null) return samlPersistentId;
|
||||
if (samlPersistentId != null)
|
||||
return samlPersistentId;
|
||||
// "G-" stands for "generated"
|
||||
samlPersistentId = "G-" + UUID.randomUUID().toString();
|
||||
user.setSingleAttribute(name, 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)
|
||||
return userSession.getUser().getUsername();
|
||||
} else {
|
||||
|
@ -297,15 +323,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
clientSession.setNote(SAML_NAME_ID_FORMAT, nameIdFormat);
|
||||
|
||||
SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder();
|
||||
builder.requestID(requestID)
|
||||
.destination(redirectUri)
|
||||
.issuer(responseIssuer)
|
||||
.assertionExpiration(realm.getAccessCodeLifespan())
|
||||
.subjectExpiration(realm.getAccessTokenLifespan())
|
||||
.sessionIndex(clientSession.getId())
|
||||
.requestIssuer(clientSession.getClient().getClientId())
|
||||
.nameIdentifier(nameIdFormat, nameId)
|
||||
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
|
||||
builder.requestID(requestID).destination(redirectUri).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)) {
|
||||
builder.disableAuthnStatement(true);
|
||||
}
|
||||
|
@ -317,20 +336,20 @@ public class SamlProtocol implements LoginProtocol {
|
|||
Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers();
|
||||
for (ProtocolMapperModel mapping : mappings) {
|
||||
|
||||
ProtocolMapper mapper = (ProtocolMapper)session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
if (mapper == null) continue;
|
||||
ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
|
||||
if (mapper == null)
|
||||
continue;
|
||||
if (mapper instanceof SAMLAttributeStatementMapper) {
|
||||
attributeStatementMappers.add(new ProtocolMapperProcessor<SAMLAttributeStatementMapper>((SAMLAttributeStatementMapper)mapper, mapping));
|
||||
attributeStatementMappers.add(new ProtocolMapperProcessor<SAMLAttributeStatementMapper>((SAMLAttributeStatementMapper) mapper, mapping));
|
||||
}
|
||||
if (mapper instanceof SAMLLoginResponseMapper) {
|
||||
loginResponseMappers.add(new ProtocolMapperProcessor<SAMLLoginResponseMapper>((SAMLLoginResponseMapper)mapper, mapping));
|
||||
loginResponseMappers.add(new ProtocolMapperProcessor<SAMLLoginResponseMapper>((SAMLLoginResponseMapper) mapper, mapping));
|
||||
}
|
||||
if (mapper instanceof SAMLRoleListMapper) {
|
||||
roleListMapper = new ProtocolMapperProcessor<SAMLRoleListMapper>((SAMLRoleListMapper)mapper, mapping);
|
||||
roleListMapper = new ProtocolMapperProcessor<SAMLRoleListMapper>((SAMLRoleListMapper) mapper, mapping);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Document samlDocument = null;
|
||||
try {
|
||||
ResponseType samlModel = builder.buildModel();
|
||||
|
@ -351,18 +370,14 @@ public class SamlProtocol implements LoginProtocol {
|
|||
if (canonicalization != null) {
|
||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||
}
|
||||
bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
|
||||
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
|
||||
}
|
||||
if (requiresAssertionSignature(client)) {
|
||||
String canonicalization = client.getAttribute(SAML_CANONICALIZATION_METHOD_ATTRIBUTE);
|
||||
if (canonicalization != null) {
|
||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||
}
|
||||
bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
|
||||
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signAssertions();
|
||||
bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signAssertions();
|
||||
}
|
||||
if (requiresEncryption(client)) {
|
||||
PublicKey publicKey = null;
|
||||
|
@ -402,7 +417,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
String alg = client.getAttribute(SAML_SIGNATURE_ALGORITHM);
|
||||
if (alg != null) {
|
||||
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg);
|
||||
if (algorithm != null) return algorithm;
|
||||
if (algorithm != null)
|
||||
return algorithm;
|
||||
}
|
||||
return SignatureAlgorithm.RSA_SHA256;
|
||||
}
|
||||
|
@ -421,10 +437,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
public void transformAttributeStatement(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers,
|
||||
ResponseType response,
|
||||
KeycloakSession session,
|
||||
UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
public void transformAttributeStatement(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers, ResponseType response, KeycloakSession session, UserSessionModel userSession,
|
||||
ClientSessionModel clientSession) {
|
||||
AssertionType assertion = response.getAssertions().get(0).getAssertion();
|
||||
AttributeStatementType attributeStatement = new AttributeStatementType();
|
||||
|
||||
|
@ -432,50 +446,32 @@ public class SamlProtocol implements LoginProtocol {
|
|||
processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession);
|
||||
}
|
||||
|
||||
//SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
|
||||
if(attributeStatement.getAttributes().size() > 0) {
|
||||
// SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
|
||||
if (attributeStatement.getAttributes().size() > 0) {
|
||||
assertion.addStatement(attributeStatement);
|
||||
}
|
||||
}
|
||||
|
||||
public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers,
|
||||
ResponseType response,
|
||||
KeycloakSession session,
|
||||
UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
for (ProtocolMapperProcessor<SAMLLoginResponseMapper> processor : mappers) {
|
||||
response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper,
|
||||
ResponseType response,
|
||||
KeycloakSession session,
|
||||
UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
if (roleListMapper == null) return;
|
||||
public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
if (roleListMapper == null)
|
||||
return;
|
||||
AssertionType assertion = response.getAssertions().get(0).getAssertion();
|
||||
AttributeStatementType attributeStatement = new AttributeStatementType();
|
||||
roleListMapper.mapper.mapRoles(attributeStatement, roleListMapper.model, session, userSession, clientSession);
|
||||
|
||||
//SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
|
||||
if(attributeStatement.getAttributes().size() > 0) {
|
||||
// SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
|
||||
if (attributeStatement.getAttributes().size() > 0) {
|
||||
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) {
|
||||
String logoutServiceUrl = null;
|
||||
if (SAML_POST_BINDING.equals(bindingType)) {
|
||||
|
@ -483,8 +479,10 @@ public class SamlProtocol implements LoginProtocol {
|
|||
} else {
|
||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
|
||||
}
|
||||
if (logoutServiceUrl == null && client instanceof ClientModel) logoutServiceUrl = ((ClientModel)client).getManagementUrl();
|
||||
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) return null;
|
||||
if (logoutServiceUrl == null && client instanceof ClientModel)
|
||||
logoutServiceUrl = ((ClientModel) client).getManagementUrl();
|
||||
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
|
||||
return null;
|
||||
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl);
|
||||
|
||||
}
|
||||
|
@ -492,7 +490,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
@Override
|
||||
public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
if (!(client instanceof ClientModel)) return null;
|
||||
if (!(client instanceof ClientModel))
|
||||
return null;
|
||||
try {
|
||||
if (isLogoutPostBindingForClient(clientSession)) {
|
||||
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
|
||||
|
@ -541,9 +540,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
if (canonicalization != null) {
|
||||
binding.canonicalizationMethod(canonicalization);
|
||||
}
|
||||
binding.signatureAlgorithm(algorithm)
|
||||
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -561,8 +558,6 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
|
@ -573,7 +568,6 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(logoutUrl, clientSession, client);
|
||||
|
||||
|
||||
String logoutRequestString = null;
|
||||
try {
|
||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(client);
|
||||
|
@ -583,20 +577,21 @@ public class SamlProtocol implements LoginProtocol {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
|
||||
for (int i = 0; i < 2; i++) { // follow redirects once
|
||||
try {
|
||||
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||
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");
|
||||
HttpPost post = new HttpPost(logoutUrl);
|
||||
post.setEntity(form);
|
||||
HttpResponse response = httpClient.execute(post);
|
||||
try {
|
||||
int status = response.getStatusLine().getStatusCode();
|
||||
if (status == 302 && !logoutUrl.endsWith("/")) {
|
||||
if (status == 302 && !logoutUrl.endsWith("/")) {
|
||||
String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
|
||||
String withSlash = logoutUrl + "/";
|
||||
if (withSlash.equals(redirect)) {
|
||||
|
@ -608,7 +603,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
HttpEntity entity = response.getEntity();
|
||||
if (entity != null) {
|
||||
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) {
|
||||
// build userPrincipal with subject used at login
|
||||
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
|
||||
.assertionExpiration(realm.getAccessCodeLifespan())
|
||||
.issuer(getResponseIssuer(realm))
|
||||
.sessionIndex(clientSession.getId())
|
||||
.userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT))
|
||||
.destination(logoutUrl);
|
||||
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm)).sessionIndex(clientSession.getId())
|
||||
.userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)).destination(logoutUrl);
|
||||
return logoutBuilder;
|
||||
}
|
||||
|
||||
private JaxrsSAML2BindingBuilder createBindingBuilder(ClientModel client) {
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
|
||||
if (requiresRealmSignature(client)) {
|
||||
binding.signatureAlgorithm(getSignatureAlgorithm(client))
|
||||
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
binding.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
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.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
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.protocol.AuthnRequestType;
|
||||
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.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
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.saml.SAML2LogoutResponseBuilder;
|
||||
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.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
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
|
||||
|
@ -67,40 +53,12 @@ import java.util.List;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SamlService {
|
||||
public class SamlService extends AuthorizationEndpointBase {
|
||||
|
||||
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) {
|
||||
this.realm = realm;
|
||||
this.event = event;
|
||||
this.authManager = authManager;
|
||||
super(realm, event, authManager);
|
||||
}
|
||||
|
||||
public abstract class BindingProtocol {
|
||||
|
@ -243,7 +201,7 @@ public class SamlService {
|
|||
bindingType = SamlProtocol.SAML_POST_BINDING;
|
||||
String redirect = null;
|
||||
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);
|
||||
} else {
|
||||
if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
|
||||
|
@ -262,7 +220,6 @@ public class SamlService {
|
|||
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
|
||||
}
|
||||
|
||||
|
||||
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
|
||||
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
|
||||
clientSession.setRedirectUri(redirect);
|
||||
|
@ -286,13 +243,9 @@ public class SamlService {
|
|||
}
|
||||
}
|
||||
|
||||
return newBrowserAuthentication(clientSession);
|
||||
return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private String getBindingType(AuthnRequestType requestAbstractType) {
|
||||
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
|
||||
|
||||
|
@ -308,10 +261,8 @@ public class SamlService {
|
|||
}
|
||||
|
||||
private boolean isSupportedNameIdFormat(String nameIdFormat) {
|
||||
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_UNSPECIFIED.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_UNSPECIFIED.get())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -340,7 +291,8 @@ public class SamlService {
|
|||
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_BINDING, logoutBinding);
|
||||
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) {
|
||||
for (String sessionIndex : logoutRequest.getSessionIndex()) {
|
||||
ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex);
|
||||
if (clientSession == null) continue;
|
||||
if (clientSession == null)
|
||||
continue;
|
||||
UserSessionModel userSession = clientSession.getUserSession();
|
||||
if (clientSession.getClient().getClientId().equals(client.getClientId())) {
|
||||
// remove requesting client from logout
|
||||
|
@ -391,13 +344,10 @@ public class SamlService {
|
|||
builder.logoutRequestID(logoutRequest.getID());
|
||||
builder.destination(logoutBindingUri);
|
||||
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||
.relayState(logoutRelayState);
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
|
||||
if (SamlProtocol.requiresRealmSignature(client)) {
|
||||
SignatureAlgorithm algorithm = SamlProtocol.getSignatureAlgorithm(client);
|
||||
binding.signatureAlgorithm(algorithm)
|
||||
.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
|
||||
|
||||
}
|
||||
try {
|
||||
|
@ -420,7 +370,6 @@ public class SamlService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
protected class PostBindingProtocol extends BindingProtocol {
|
||||
|
||||
@Override
|
||||
|
@ -443,12 +392,14 @@ public class SamlService {
|
|||
return SamlProtocol.SAML_POST_BINDING;
|
||||
}
|
||||
|
||||
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||
Response response = basicChecks(samlRequest, samlResponse);
|
||||
if (response != null) return response;
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
if (response != null)
|
||||
return response;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
|
||||
|
@ -480,74 +430,35 @@ public class SamlService {
|
|||
return SamlProtocol.SAML_REDIRECT_BINDING;
|
||||
}
|
||||
|
||||
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||
Response response = basicChecks(samlRequest, samlResponse);
|
||||
if (response != null) return response;
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
if (response != null)
|
||||
return response;
|
||||
if (samlRequest != null)
|
||||
return handleSamlRequest(samlRequest, relayState);
|
||||
else
|
||||
return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
|
||||
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, boolean isPassive) {
|
||||
return handleBrowserAuthenticationRequest(clientSession, new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo), isPassive);
|
||||
}
|
||||
|
||||
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
|
||||
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
logger.debug("SAML GET");
|
||||
return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
logger.debug("SAML POST");
|
||||
return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
@ -570,13 +481,13 @@ public class SamlService {
|
|||
@GET
|
||||
@Path("clients/{client}")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public Response idpInitiatedSSO(@PathParam("client") String clientUrlName,
|
||||
@QueryParam("RelayState") String relayState) {
|
||||
public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, @QueryParam("RelayState") String relayState) {
|
||||
event.event(EventType.LOGIN);
|
||||
ClientModel client = null;
|
||||
for (ClientModel c : realm.getClients()) {
|
||||
String urlName = c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME);
|
||||
if (urlName == null) continue;
|
||||
if (urlName == null)
|
||||
continue;
|
||||
if (urlName.equals(clientUrlName)) {
|
||||
client = c;
|
||||
break;
|
||||
|
@ -586,18 +497,14 @@ public class SamlService {
|
|||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND);
|
||||
}
|
||||
if (client.getManagementUrl() == null
|
||||
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null
|
||||
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == 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) {
|
||||
logger.error("SAML assertion consumer url not set up");
|
||||
event.error(Errors.INVALID_REDIRECT_URI);
|
||||
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
|
||||
}
|
||||
|
||||
String bindingType = SamlProtocol.SAML_POST_BINDING;
|
||||
if (client.getManagementUrl() == null
|
||||
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null
|
||||
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != 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) {
|
||||
bindingType = SamlProtocol.SAML_REDIRECT_BINDING;
|
||||
}
|
||||
|
||||
|
@ -626,8 +533,7 @@ public class SamlService {
|
|||
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
|
||||
}
|
||||
|
||||
|
||||
return newBrowserAuthentication(clientSession);
|
||||
return newBrowserAuthentication(clientSession, false);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocol.Error;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
|
@ -470,7 +471,7 @@ public class AuthenticationProcessor {
|
|||
protocol.setRealm(getRealm())
|
||||
.setHttpHeaders(getHttpRequest().getHttpHeaders())
|
||||
.setUriInfo(getUriInfo());
|
||||
Response response = protocol.cancelLogin(getClientSession());
|
||||
Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER);
|
||||
forceChallenge(response);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,28 @@ import javax.ws.rs.core.UriInfo;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
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 setRealm(RealmModel realm);
|
||||
|
@ -27,11 +49,12 @@ public interface LoginProtocol extends Provider {
|
|||
|
||||
LoginProtocol setEventBuilder(EventBuilder event);
|
||||
|
||||
Response cancelLogin(ClientSessionModel clientSession);
|
||||
Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode);
|
||||
Response consentDenied(ClientSessionModel clientSession);
|
||||
|
||||
Response sendError(ClientSessionModel clientSession, Error error);
|
||||
|
||||
void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
|
||||
Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
|
||||
Response finishLogout(UserSessionModel userSession);
|
||||
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
this.event = event;
|
||||
}
|
||||
|
||||
public OIDCLoginProtocol(){
|
||||
public OIDCLoginProtocol() {
|
||||
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers){
|
||||
public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers) {
|
||||
this.headers = headers;
|
||||
return this;
|
||||
}
|
||||
|
@ -116,19 +116,6 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
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
|
||||
public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
|
||||
ClientSessionModel clientSession = accessCode.getClientSession();
|
||||
|
@ -144,10 +131,11 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
return location.build();
|
||||
}
|
||||
|
||||
public Response consentDenied(ClientSessionModel clientSession) {
|
||||
@Override
|
||||
public Response sendError(ClientSessionModel clientSession, Error error) {
|
||||
String redirect = clientSession.getRedirectUri();
|
||||
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)
|
||||
redirectUri.queryParam(OAuth2Constants.STATE, state);
|
||||
session.sessions().removeClientSession(realm, clientSession);
|
||||
|
@ -156,20 +144,25 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
return location.build();
|
||||
}
|
||||
|
||||
|
||||
public Response invalidSessionError(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);
|
||||
private String translateError(Error error) {
|
||||
switch (error) {
|
||||
case CANCELLED_BY_USER:
|
||||
case CONSENT_DENIED:
|
||||
return "access_denied";
|
||||
case PASSIVE_INTERACTION_REQUIRED:
|
||||
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
|
||||
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
if (!(clientSession.getClient() instanceof ClientModel)) return;
|
||||
if (!(clientSession.getClient() instanceof ClientModel))
|
||||
return;
|
||||
ClientModel app = clientSession.getClient();
|
||||
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();
|
||||
|
||||
|
||||
if (redirectUri != null) {
|
||||
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();
|
||||
} else {
|
||||
return Response.ok().build();
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
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.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
|
@ -18,6 +27,7 @@ import org.keycloak.models.IdentityProviderModel;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.RestartLoginCookie;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
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.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>
|
||||
*/
|
||||
public class AuthorizationEndpoint {
|
||||
public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class);
|
||||
|
||||
public static final String CODE_AUTH_TYPE = "code";
|
||||
|
||||
private enum Action {
|
||||
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 ClientSessionModel clientSession;
|
||||
|
||||
|
@ -86,9 +70,7 @@ public class AuthorizationEndpoint {
|
|||
private String legacyResponseType;
|
||||
|
||||
public AuthorizationEndpoint(AuthenticationManager authManager, RealmModel realm, EventBuilder event) {
|
||||
this.authManager = authManager;
|
||||
this.realm = realm;
|
||||
this.event = event;
|
||||
super(realm, event, authManager);
|
||||
event.event(EventType.LOGIN);
|
||||
}
|
||||
|
||||
|
@ -249,7 +231,6 @@ public class AuthorizationEndpoint {
|
|||
}
|
||||
|
||||
private Response buildAuthorizationCodeAuthorizationResponse() {
|
||||
String accessCode = new ClientSessionCode(realm, clientSession).getCode();
|
||||
|
||||
if (idpHint != null && !"".equals(idpHint)) {
|
||||
IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint);
|
||||
|
@ -259,65 +240,13 @@ public class AuthorizationEndpoint {
|
|||
.setError(Messages.IDENTITY_PROVIDER_NOT_FOUND, idpHint)
|
||||
.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);
|
||||
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);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), prompt != null && prompt.equals("none"));
|
||||
}
|
||||
|
||||
private Response buildRegister() {
|
||||
|
@ -326,7 +255,7 @@ public class AuthorizationEndpoint {
|
|||
AuthenticationFlowModel flow = realm.getRegistrationFlow();
|
||||
String flowId = flow.getId();
|
||||
|
||||
AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.REGISTRATION_PATH);
|
||||
AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.REGISTRATION_PATH);
|
||||
|
||||
return processor.authenticate();
|
||||
}
|
||||
|
@ -337,32 +266,12 @@ public class AuthorizationEndpoint {
|
|||
AuthenticationFlowModel flow = realm.getResetCredentialsFlow();
|
||||
String flowId = flow.getId();
|
||||
|
||||
AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
|
||||
AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -36,6 +36,7 @@ import org.keycloak.login.LoginFormsProvider;
|
|||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocol.Error;
|
||||
import org.keycloak.protocol.RestartLoginCookie;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
@ -523,7 +524,7 @@ public class AuthenticationManager {
|
|||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
||||
.setUriInfo(context.getUriInfo());
|
||||
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) {
|
||||
clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId());
|
||||
|
|
|
@ -57,6 +57,7 @@ import org.keycloak.models.utils.FormMessage;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.RestartLoginCookie;
|
||||
import org.keycloak.protocol.LoginProtocol.Error;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.Urls;
|
||||
|
@ -591,7 +592,7 @@ public class LoginActionsService {
|
|||
.setHttpHeaders(headers)
|
||||
.setUriInfo(uriInfo);
|
||||
event.error(Errors.REJECTED_BY_USER);
|
||||
return protocol.consentDenied(clientSession);
|
||||
return protocol.sendError(clientSession, Error.CONSENT_DENIED);
|
||||
}
|
||||
|
||||
UserConsentModel grantedConsent = user.getConsentByClient(client.getId());
|
||||
|
@ -828,7 +829,7 @@ public class LoginActionsService {
|
|||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
||||
.setUriInfo(context.getUriInfo());
|
||||
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");
|
||||
|
|
|
@ -19,6 +19,7 @@ public class SamlAdapterTest {
|
|||
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
|
||||
|
||||
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-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);
|
||||
|
@ -96,6 +97,11 @@ public class SamlAdapterTest {
|
|||
testStrategy.testPostSimpleLoginLogout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostPassiveLoginLogout() {
|
||||
testStrategy.testPostPassiveLoginLogout(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostSignedLoginLogoutTransientNameID() {
|
||||
testStrategy.testPostSignedLoginLogoutTransientNameID();
|
||||
|
|
|
@ -139,6 +139,38 @@ public class SamlAdapterTestStrategy extends ExternalResource {
|
|||
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) {
|
||||
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
|
||||
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
|
||||
|
|
|
@ -6,7 +6,6 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
|
@ -20,10 +19,17 @@ public class ErrorServlet extends HttpServlet {
|
|||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
authError = (AuthenticationError)req.getAttribute(AuthenticationError.class.getName());
|
||||
|
||||
Integer statusCode = (Integer) req.getAttribute("javax.servlet.error.status_code");
|
||||
|
||||
resp.setContentType("text/html");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
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();
|
||||
|
||||
|
||||
|
|
|
@ -1,35 +1,24 @@
|
|||
package org.keycloak.testsuite.saml;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
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.HardcodedAttributeMapper;
|
||||
import org.keycloak.protocol.saml.mappers.HardcodedRole;
|
||||
import org.keycloak.protocol.saml.mappers.RoleListMapper;
|
||||
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.admin.AdminRoot;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||
import org.keycloak.testsuite.rule.WebResource;
|
||||
import org.keycloak.testsuite.rule.WebRule;
|
||||
|
@ -47,19 +36,10 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
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.UriBuilder;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
@ -166,6 +146,7 @@ public class SamlBindingTest {
|
|||
driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
|
||||
checkLoggedOut("http://localhost:8081/sales-post/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostSimpleLoginLogoutIdpInitiated() {
|
||||
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/");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostSignedLoginLogoutTransientNameID() {
|
||||
driver.navigate().to("http://localhost:8081/sales-post-sig-transient/");
|
||||
|
@ -452,23 +434,10 @@ public class SamlBindingTest {
|
|||
Assert.assertTrue(driver.getPageSource().contains("null"));
|
||||
}
|
||||
|
||||
private static String createToken() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
|
||||
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);
|
||||
}
|
||||
@Test
|
||||
public void testPassiveMode() {
|
||||
// KEYCLOAK-2075 test SAML IsPassive handling - PicketLink SP client library doesn't support this option unfortunately.
|
||||
// But the test of server side is included in test of SAML Keycloak adapter
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import org.junit.ClassRule;
|
|||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy;
|
||||
import org.keycloak.testsuite.keycloaksaml.SamlSPFacade;
|
||||
import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
|
@ -25,6 +24,7 @@ public class SamlAdapterTest {
|
|||
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
|
||||
|
||||
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-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);
|
||||
|
@ -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/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
|
||||
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -105,6 +102,11 @@ public class SamlAdapterTest {
|
|||
testStrategy.testPostSimpleLoginLogout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostPassiveLoginLogout() {
|
||||
testStrategy.testPostPassiveLoginLogout(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostSignedLoginLogoutTransientNameID() {
|
||||
testStrategy.testPostSignedLoginLogoutTransientNameID();
|
||||
|
|
|
@ -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>
|
|
@ -85,6 +85,24 @@
|
|||
"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/",
|
||||
"enabled": true,
|
||||
|
|
Loading…
Reference in a new issue