KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library
KEYCLOAK-2075 - added integration tests for both server and adapter side
This commit is contained in:
parent
0bdb05e152
commit
18fa03bf97
28 changed files with 318 additions and 164 deletions
|
@ -11,7 +11,8 @@
|
||||||
sslPolicy="EXTERNAL"
|
sslPolicy="EXTERNAL"
|
||||||
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||||
logoutPage="/logout.jsp"
|
logoutPage="/logout.jsp"
|
||||||
forceAuthentication="false">
|
forceAuthentication="false"
|
||||||
|
isPassive="false">
|
||||||
<Keys>
|
<Keys>
|
||||||
<Key signing="true" >
|
<Key signing="true" >
|
||||||
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
|
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
|
||||||
|
@ -63,7 +64,8 @@
|
||||||
<SP entityID="sp"
|
<SP entityID="sp"
|
||||||
sslPolicy="ssl"
|
sslPolicy="ssl"
|
||||||
nameIDPolicyFormat="format"
|
nameIDPolicyFormat="format"
|
||||||
forceAuthentication="true">
|
forceAuthentication="true"
|
||||||
|
isPassive="false">
|
||||||
...
|
...
|
||||||
</SP>]]></programlisting>
|
</SP>]]></programlisting>
|
||||||
<para>
|
<para>
|
||||||
|
@ -106,12 +108,23 @@
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
SAML clients can request that a user is re-authenticated even if
|
SAML clients can request that a user is re-authenticated even if
|
||||||
they are already logged in at the IDP. Set this to true if you
|
they are already logged in at the IDP. Set this to <literal>true</literal> if you
|
||||||
want this.
|
want this.
|
||||||
<emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
|
<emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
|
<varlistentry>
|
||||||
|
<term>isPassive</term>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
SAML clients can request that a user is never asked to authenticate even if
|
||||||
|
they are not logged in at the IDP. Set this to <literal>true</literal> if you want this.
|
||||||
|
Do not use together with <literal>forceAuthentication</literal> as they are opposite.
|
||||||
|
<emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
|
</varlistentry>
|
||||||
</variablelist>
|
</variablelist>
|
||||||
</para>
|
</para>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<chapter id="tomcat-adapter">
|
<chapter id="tomcat-adapter">
|
||||||
<title>Tomcat 6, 7 and 8 SAML dapters</title>
|
<title>Tomcat 6, 7 and 8 SAML adapters</title>
|
||||||
<para>
|
<para>
|
||||||
To be able to secure WAR apps deployed on Tomcat 6, 7 and 8 you must install the Keycloak Tomcat 6, 7 or 8 SAML adapter
|
To be able to secure WAR apps deployed on Tomcat 6, 7 and 8 you must install the Keycloak Tomcat 6, 7 or 8 SAML adapter
|
||||||
into your Tomcat installation. You then have to provide some extra configuration in each WAR you deploy to
|
into your Tomcat installation. You then have to provide some extra configuration in each WAR you deploy to
|
||||||
|
|
|
@ -5,8 +5,5 @@ package org.keycloak.adapters.spi;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public enum AuthOutcome {
|
public enum AuthOutcome {
|
||||||
NOT_ATTEMPTED,
|
NOT_ATTEMPTED, FAILED, AUTHENTICATED, NOT_AUTHENTICATED, LOGGED_OUT
|
||||||
FAILED,
|
|
||||||
AUTHENTICATED,
|
|
||||||
LOGGED_OUT
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package org.keycloak.adapters.saml;
|
package org.keycloak.adapters.saml;
|
||||||
|
|
||||||
import org.keycloak.common.enums.SslRequired;
|
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.keycloak.common.enums.SslRequired;
|
||||||
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
|
@ -31,7 +31,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
return validateResponseSignature;
|
return validateResponseSignature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Binding getRequestBinding() {
|
public Binding getRequestBinding() {
|
||||||
return requestBinding;
|
return requestBinding;
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
return signResponse;
|
return signResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Binding getRequestBinding() {
|
public Binding getRequestBinding() {
|
||||||
return requestBinding;
|
return requestBinding;
|
||||||
}
|
}
|
||||||
|
@ -150,12 +150,8 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static class DefaultIDP implements IDP {
|
public static class DefaultIDP implements IDP {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private String entityID;
|
private String entityID;
|
||||||
private PublicKey signatureValidationKey;
|
private PublicKey signatureValidationKey;
|
||||||
private SingleSignOnService singleSignOnService;
|
private SingleSignOnService singleSignOnService;
|
||||||
|
@ -204,6 +200,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
private String entityID;
|
private String entityID;
|
||||||
private String nameIDPolicyFormat;
|
private String nameIDPolicyFormat;
|
||||||
private boolean forceAuthentication;
|
private boolean forceAuthentication;
|
||||||
|
private boolean isPassive;
|
||||||
private PrivateKey decryptionKey;
|
private PrivateKey decryptionKey;
|
||||||
private KeyPair signingKeyPair;
|
private KeyPair signingKeyPair;
|
||||||
private String assertionConsumerServiceUrl;
|
private String assertionConsumerServiceUrl;
|
||||||
|
@ -214,7 +211,6 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
private SignatureAlgorithm signatureAlgorithm;
|
private SignatureAlgorithm signatureAlgorithm;
|
||||||
private String signatureCanonicalizationMethod;
|
private String signatureCanonicalizationMethod;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IDP getIDP() {
|
public IDP getIDP() {
|
||||||
return idp;
|
return idp;
|
||||||
|
@ -244,6 +240,11 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
public boolean isForceAuthentication() {
|
public boolean isForceAuthentication() {
|
||||||
return forceAuthentication;
|
return forceAuthentication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isIsPassive() {
|
||||||
|
return isPassive;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PrivateKey getDecryptionKey() {
|
public PrivateKey getDecryptionKey() {
|
||||||
|
@ -265,7 +266,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
return roleAttributeNames;
|
return roleAttributeNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PrincipalNamePolicy getPrincipalNamePolicy() {
|
public PrincipalNamePolicy getPrincipalNamePolicy() {
|
||||||
return principalNamePolicy;
|
return principalNamePolicy;
|
||||||
}
|
}
|
||||||
|
@ -298,6 +299,10 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
public void setForceAuthentication(boolean forceAuthentication) {
|
public void setForceAuthentication(boolean forceAuthentication) {
|
||||||
this.forceAuthentication = forceAuthentication;
|
this.forceAuthentication = forceAuthentication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setIsPassive(boolean isPassive){
|
||||||
|
this.isPassive = isPassive;
|
||||||
|
}
|
||||||
|
|
||||||
public void setDecryptionKey(PrivateKey decryptionKey) {
|
public void setDecryptionKey(PrivateKey decryptionKey) {
|
||||||
this.decryptionKey = decryptionKey;
|
this.decryptionKey = decryptionKey;
|
||||||
|
@ -332,7 +337,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
this.logoutPage = logoutPage;
|
this.logoutPage = logoutPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getSignatureCanonicalizationMethod() {
|
public String getSignatureCanonicalizationMethod() {
|
||||||
return signatureCanonicalizationMethod;
|
return signatureCanonicalizationMethod;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ public class InitiateLogin implements AuthChallenge {
|
||||||
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
||||||
.destination(destinationUrl)
|
.destination(destinationUrl)
|
||||||
.issuer(issuerURL)
|
.issuer(issuerURL)
|
||||||
.forceAuthn(deployment.isForceAuthentication())
|
.forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive())
|
||||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
||||||
if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) {
|
if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) {
|
||||||
String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();
|
String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();
|
||||||
|
|
|
@ -17,7 +17,9 @@ import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusType;
|
||||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||||
|
@ -27,6 +29,7 @@ import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
import org.keycloak.saml.common.util.Base64;
|
import org.keycloak.saml.common.util.Base64;
|
||||||
|
import org.keycloak.saml.common.util.StringUtil;
|
||||||
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
||||||
|
@ -207,7 +210,26 @@ public abstract class SamlAuthenticator {
|
||||||
log.error("Request URI does not match SAML request destination");
|
log.error("Request URI does not match SAML request destination");
|
||||||
return AuthOutcome.FAILED;
|
return AuthOutcome.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusResponse instanceof ResponseType) {
|
if (statusResponse instanceof ResponseType) {
|
||||||
|
|
||||||
|
//validate status
|
||||||
|
StatusType status = statusResponse.getStatus();
|
||||||
|
if(status == null){
|
||||||
|
log.error("Missing Status in SAML response");
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
if(!checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_SUCCESS.get())){
|
||||||
|
if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
|
||||||
|
// KEYCLOAK-2107 - handle user not authenticated due passive mode
|
||||||
|
log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
|
||||||
|
return AuthOutcome.NOT_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
log.error("Error Status found in SAML response: " + status.toString());
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
|
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
|
||||||
try {
|
try {
|
||||||
|
@ -287,7 +309,16 @@ public abstract class SamlAuthenticator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
|
||||||
|
if(statusCode != null && statusCode.getValue()!=null){
|
||||||
|
String v = statusCode.getValue().toString();
|
||||||
|
return expectedValue.equals(v);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected AuthOutcome handleLoginResponse(ResponseType responseType) {
|
protected AuthOutcome handleLoginResponse(ResponseType responseType) {
|
||||||
|
|
||||||
AssertionType assertion = null;
|
AssertionType assertion = null;
|
||||||
try {
|
try {
|
||||||
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
|
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
|
||||||
|
@ -295,7 +326,7 @@ public abstract class SamlAuthenticator {
|
||||||
return initiateLogin();
|
return initiateLogin();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error extracting SAML assertion, e");
|
log.error("Error extracting SAML assertion: " + e.getMessage());
|
||||||
challenge = new AuthChallenge() {
|
challenge = new AuthChallenge() {
|
||||||
@Override
|
@Override
|
||||||
public boolean challenge(HttpFacade exchange) {
|
public boolean challenge(HttpFacade exchange) {
|
||||||
|
@ -434,9 +465,9 @@ public abstract class SamlAuthenticator {
|
||||||
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected SAMLDocumentHolder extractPostBindingResponse(String response) {
|
protected SAMLDocumentHolder extractPostBindingResponse(String response) {
|
||||||
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
||||||
String xml = new String(samlBytes);
|
|
||||||
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ public interface SamlDeployment {
|
||||||
String getEntityID();
|
String getEntityID();
|
||||||
String getNameIDPolicyFormat();
|
String getNameIDPolicyFormat();
|
||||||
boolean isForceAuthentication();
|
boolean isForceAuthentication();
|
||||||
|
boolean isIsPassive();
|
||||||
PrivateKey getDecryptionKey();
|
PrivateKey getDecryptionKey();
|
||||||
KeyPair getSigningKeyPair();
|
KeyPair getSigningKeyPair();
|
||||||
String getSignatureCanonicalizationMethod();
|
String getSignatureCanonicalizationMethod();
|
||||||
|
|
|
@ -33,6 +33,7 @@ public class SP implements Serializable {
|
||||||
private String entityID;
|
private String entityID;
|
||||||
private String sslPolicy;
|
private String sslPolicy;
|
||||||
private boolean forceAuthentication;
|
private boolean forceAuthentication;
|
||||||
|
private boolean isPassive;
|
||||||
private String logoutPage;
|
private String logoutPage;
|
||||||
private List<Key> keys;
|
private List<Key> keys;
|
||||||
private String nameIDPolicyFormat;
|
private String nameIDPolicyFormat;
|
||||||
|
@ -64,6 +65,14 @@ public class SP implements Serializable {
|
||||||
this.forceAuthentication = forceAuthentication;
|
this.forceAuthentication = forceAuthentication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isIsPassive() {
|
||||||
|
return isPassive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsPassive(boolean isPassive) {
|
||||||
|
this.isPassive = isPassive;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Key> getKeys() {
|
public List<Key> getKeys() {
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,17 +5,17 @@ package org.keycloak.adapters.saml.config.parsers;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class ConfigXmlConstants {
|
public class ConfigXmlConstants {
|
||||||
public static final String KEYCLOAK_SAML_ADAPTER ="keycloak-saml-adapter";
|
public static final String KEYCLOAK_SAML_ADAPTER = "keycloak-saml-adapter";
|
||||||
public static final String SP_ELEMENT="SP";
|
public static final String SP_ELEMENT = "SP";
|
||||||
public static final String ENTITY_ID_ATTR = "entityID";
|
public static final String ENTITY_ID_ATTR = "entityID";
|
||||||
public static final String SSL_POLICY_ATTR = "sslPolicy";
|
public static final String SSL_POLICY_ATTR = "sslPolicy";
|
||||||
public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat";
|
public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat";
|
||||||
public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication";
|
public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication";
|
||||||
|
public static final String IS_PASSIVE_ATTR = "isPassive";
|
||||||
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
|
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
|
||||||
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
|
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
|
||||||
public static final String LOGOUT_PAGE_ATTR = "logoutPage";
|
public static final String LOGOUT_PAGE_ATTR = "logoutPage";
|
||||||
|
|
||||||
|
|
||||||
public static final String KEYS_ELEMENT = "Keys";
|
public static final String KEYS_ELEMENT = "Keys";
|
||||||
public static final String KEY_ELEMENT = "Key";
|
public static final String KEY_ELEMENT = "Key";
|
||||||
public static final String SIGNING_ATTR = "signing";
|
public static final String SIGNING_ATTR = "signing";
|
||||||
|
@ -36,7 +36,6 @@ public class ConfigXmlConstants {
|
||||||
public static final String POLICY_ATTR = "policy";
|
public static final String POLICY_ATTR = "policy";
|
||||||
public static final String ATTRIBUTE_ATTR = "attribute";
|
public static final String ATTRIBUTE_ATTR = "attribute";
|
||||||
|
|
||||||
|
|
||||||
public static final String ROLE_IDENTIFIERS_ELEMENT = "RoleIdentifiers";
|
public static final String ROLE_IDENTIFIERS_ELEMENT = "RoleIdentifiers";
|
||||||
public static final String ATTRIBUTE_ELEMENT = "Attribute";
|
public static final String ATTRIBUTE_ELEMENT = "Attribute";
|
||||||
public static final String NAME_ATTR = "name";
|
public static final String NAME_ATTR = "name";
|
||||||
|
|
|
@ -41,6 +41,7 @@ public class DeploymentBuilder {
|
||||||
deployment.setConfigured(true);
|
deployment.setConfigured(true);
|
||||||
deployment.setEntityID(sp.getEntityID());
|
deployment.setEntityID(sp.getEntityID());
|
||||||
deployment.setForceAuthentication(sp.isForceAuthentication());
|
deployment.setForceAuthentication(sp.isForceAuthentication());
|
||||||
|
deployment.setIsPassive(sp.isIsPassive());
|
||||||
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
|
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
|
||||||
deployment.setLogoutPage(sp.getLogoutPage());
|
deployment.setLogoutPage(sp.getLogoutPage());
|
||||||
deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
|
deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
package org.keycloak.adapters.saml.config.parsers;
|
package org.keycloak.adapters.saml.config.parsers;
|
||||||
|
|
||||||
import org.keycloak.adapters.saml.config.IDP;
|
import java.util.HashSet;
|
||||||
import org.keycloak.adapters.saml.config.Key;
|
import java.util.List;
|
||||||
import org.keycloak.adapters.saml.config.SP;
|
import java.util.Set;
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
|
||||||
import org.keycloak.saml.common.parsers.AbstractParser;
|
|
||||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
|
||||||
import org.keycloak.common.util.StringPropertyReplacer;
|
|
||||||
|
|
||||||
import javax.xml.namespace.QName;
|
import javax.xml.namespace.QName;
|
||||||
import javax.xml.stream.XMLEventReader;
|
import javax.xml.stream.XMLEventReader;
|
||||||
import javax.xml.stream.events.EndElement;
|
import javax.xml.stream.events.EndElement;
|
||||||
import javax.xml.stream.events.StartElement;
|
import javax.xml.stream.events.StartElement;
|
||||||
import javax.xml.stream.events.XMLEvent;
|
import javax.xml.stream.events.XMLEvent;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import org.keycloak.adapters.saml.config.IDP;
|
||||||
import java.util.Set;
|
import org.keycloak.adapters.saml.config.Key;
|
||||||
|
import org.keycloak.adapters.saml.config.SP;
|
||||||
|
import org.keycloak.common.util.StringPropertyReplacer;
|
||||||
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
import org.keycloak.saml.common.parsers.AbstractParser;
|
||||||
|
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -25,13 +26,16 @@ public class SPXmlParser extends AbstractParser {
|
||||||
|
|
||||||
public static String getAttributeValue(StartElement startElement, String tag) {
|
public static String getAttributeValue(StartElement startElement, String tag) {
|
||||||
String str = StaxParserUtil.getAttributeValue(startElement, tag);
|
String str = StaxParserUtil.getAttributeValue(startElement, tag);
|
||||||
if (str != null) return StringPropertyReplacer.replaceProperties(str);
|
if (str != null)
|
||||||
else return str;
|
return StringPropertyReplacer.replaceProperties(str);
|
||||||
|
else
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
|
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
|
||||||
String result = getAttributeValue(startElement, tag);
|
String result = getAttributeValue(startElement, tag);
|
||||||
if (result == null) return defaultValue;
|
if (result == null)
|
||||||
|
return defaultValue;
|
||||||
return Boolean.valueOf(result);
|
return Boolean.valueOf(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,11 +45,11 @@ public class SPXmlParser extends AbstractParser {
|
||||||
|
|
||||||
public static String getElementText(XMLEventReader xmlEventReader) throws ParsingException {
|
public static String getElementText(XMLEventReader xmlEventReader) throws ParsingException {
|
||||||
String result = StaxParserUtil.getElementText(xmlEventReader);
|
String result = StaxParserUtil.getElementText(xmlEventReader);
|
||||||
if (result != null) result = StringPropertyReplacer.replaceProperties(result);
|
if (result != null)
|
||||||
|
result = StringPropertyReplacer.replaceProperties(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object parse(XMLEventReader xmlEventReader) throws ParsingException {
|
public Object parse(XMLEventReader xmlEventReader) throws ParsingException {
|
||||||
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
|
@ -61,6 +65,7 @@ public class SPXmlParser extends AbstractParser {
|
||||||
sp.setLogoutPage(getAttributeValue(startElement, ConfigXmlConstants.LOGOUT_PAGE_ATTR));
|
sp.setLogoutPage(getAttributeValue(startElement, ConfigXmlConstants.LOGOUT_PAGE_ATTR));
|
||||||
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
|
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
|
||||||
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
|
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
|
||||||
|
sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
|
||||||
while (xmlEventReader.hasNext()) {
|
while (xmlEventReader.hasNext()) {
|
||||||
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||||
if (xmlEvent == null)
|
if (xmlEvent == null)
|
||||||
|
@ -79,7 +84,7 @@ public class SPXmlParser extends AbstractParser {
|
||||||
String tag = StaxParserUtil.getStartElementName(startElement);
|
String tag = StaxParserUtil.getStartElementName(startElement);
|
||||||
if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
|
if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
|
||||||
KeysXmlParser parser = new KeysXmlParser();
|
KeysXmlParser parser = new KeysXmlParser();
|
||||||
List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
|
List<Key> keys = (List<Key>) parser.parse(xmlEventReader);
|
||||||
sp.setKeys(keys);
|
sp.setKeys(keys);
|
||||||
} else if (tag.equals(ConfigXmlConstants.PRINCIPAL_NAME_MAPPING_ELEMENT)) {
|
} else if (tag.equals(ConfigXmlConstants.PRINCIPAL_NAME_MAPPING_ELEMENT)) {
|
||||||
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
|
@ -98,7 +103,7 @@ public class SPXmlParser extends AbstractParser {
|
||||||
parseRoleMapping(xmlEventReader, sp);
|
parseRoleMapping(xmlEventReader, sp);
|
||||||
} else if (tag.equals(ConfigXmlConstants.IDP_ELEMENT)) {
|
} else if (tag.equals(ConfigXmlConstants.IDP_ELEMENT)) {
|
||||||
IDPXmlParser parser = new IDPXmlParser();
|
IDPXmlParser parser = new IDPXmlParser();
|
||||||
IDP idp = (IDP)parser.parse(xmlEventReader);
|
IDP idp = (IDP) parser.parse(xmlEventReader);
|
||||||
sp.setIdp(idp);
|
sp.setIdp(idp);
|
||||||
} else {
|
} else {
|
||||||
StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
|
StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
|
||||||
|
@ -108,7 +113,7 @@ public class SPXmlParser extends AbstractParser {
|
||||||
return sp;
|
return sp;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException {
|
protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException {
|
||||||
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||||
StaxParserUtil.validate(startElement, ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT);
|
StaxParserUtil.validate(startElement, ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT);
|
||||||
Set<String> roleAttributes = new HashSet<>();
|
Set<String> roleAttributes = new HashSet<>();
|
||||||
|
@ -144,7 +149,6 @@ public class SPXmlParser extends AbstractParser {
|
||||||
sp.setRoleAttributes(roleAttributes);
|
sp.setRoleAttributes(roleAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(QName qname) {
|
public boolean supports(QName qname) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional"/>
|
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional"/>
|
||||||
<xs:attribute name="logoutPage" type="xs:string" use="optional"/>
|
<xs:attribute name="logoutPage" type="xs:string" use="optional"/>
|
||||||
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional"/>
|
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="isPassive" type="xs:boolean" use="optional"/>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="keys-type">
|
<xs:complexType name="keys-type">
|
||||||
|
|
|
@ -68,6 +68,7 @@ public class XmlParserTest {
|
||||||
Assert.assertEquals("ssl", sp.getSslPolicy());
|
Assert.assertEquals("ssl", sp.getSslPolicy());
|
||||||
Assert.assertEquals("format", sp.getNameIDPolicyFormat());
|
Assert.assertEquals("format", sp.getNameIDPolicyFormat());
|
||||||
Assert.assertTrue(sp.isForceAuthentication());
|
Assert.assertTrue(sp.isForceAuthentication());
|
||||||
|
Assert.assertTrue(sp.isIsPassive());
|
||||||
Assert.assertEquals(2, sp.getKeys().size());
|
Assert.assertEquals(2, sp.getKeys().size());
|
||||||
Key signing = sp.getKeys().get(0);
|
Key signing = sp.getKeys().get(0);
|
||||||
Assert.assertTrue(signing.isSigning());
|
Assert.assertTrue(signing.isSigning());
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
<SP entityID="sp"
|
<SP entityID="sp"
|
||||||
sslPolicy="ssl"
|
sslPolicy="ssl"
|
||||||
nameIDPolicyFormat="format"
|
nameIDPolicyFormat="format"
|
||||||
forceAuthentication="true">
|
forceAuthentication="true"
|
||||||
|
isPassive="true">
|
||||||
<Keys>
|
<Keys>
|
||||||
<Key signing="true" >
|
<Key signing="true" >
|
||||||
<KeyStore file="file" resource="cp" password="pw">
|
<KeyStore file="file" resource="cp" password="pw">
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
nameIDPolicyFormat="format"
|
nameIDPolicyFormat="format"
|
||||||
signatureAlgorithm=""
|
signatureAlgorithm=""
|
||||||
signatureCanonicalizationMethod=""
|
signatureCanonicalizationMethod=""
|
||||||
forceAuthentication="true">
|
forceAuthentication="true"
|
||||||
|
isPassive="true">
|
||||||
<Keys>
|
<Keys>
|
||||||
<Key signing="true" >
|
<Key signing="true" >
|
||||||
<KeyStore file="file" resource="cp" password="pw">
|
<KeyStore file="file" resource="cp" password="pw">
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
package org.keycloak.adapters.saml.servlet;
|
package org.keycloak.adapters.saml.servlet;
|
||||||
|
|
||||||
import org.keycloak.adapters.spi.AuthChallenge;
|
import java.io.FileInputStream;
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
import java.io.FileNotFoundException;
|
||||||
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
|
import java.io.IOException;
|
||||||
import org.keycloak.adapters.spi.SessionIdMapper;
|
import java.io.InputStream;
|
||||||
import org.keycloak.adapters.saml.DefaultSamlDeployment;
|
import java.util.logging.Level;
|
||||||
import org.keycloak.adapters.saml.SamlAuthenticator;
|
import java.util.logging.Logger;
|
||||||
import org.keycloak.adapters.saml.SamlDeployment;
|
|
||||||
import org.keycloak.adapters.saml.SamlDeploymentContext;
|
|
||||||
import org.keycloak.adapters.saml.SamlSession;
|
|
||||||
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
|
|
||||||
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
|
|
||||||
import org.keycloak.adapters.servlet.ServletHttpFacade;
|
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
|
@ -24,12 +17,20 @@ import javax.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequestWrapper;
|
import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
import org.keycloak.adapters.saml.DefaultSamlDeployment;
|
||||||
import java.io.IOException;
|
import org.keycloak.adapters.saml.SamlAuthenticator;
|
||||||
import java.io.InputStream;
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
import java.util.logging.Level;
|
import org.keycloak.adapters.saml.SamlDeploymentContext;
|
||||||
import java.util.logging.Logger;
|
import org.keycloak.adapters.saml.SamlSession;
|
||||||
|
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
|
||||||
|
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
|
||||||
|
import org.keycloak.adapters.servlet.ServletHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
|
||||||
|
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -38,7 +39,7 @@ import java.util.logging.Logger;
|
||||||
public class SamlFilter implements Filter {
|
public class SamlFilter implements Filter {
|
||||||
protected SamlDeploymentContext deploymentContext;
|
protected SamlDeploymentContext deploymentContext;
|
||||||
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||||
private final static Logger log = Logger.getLogger(""+SamlFilter.class);
|
private final static Logger log = Logger.getLogger("" + SamlFilter.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(final FilterConfig filterConfig) throws ServletException {
|
public void init(final FilterConfig filterConfig) throws ServletException {
|
||||||
|
@ -46,12 +47,14 @@ public class SamlFilter implements Filter {
|
||||||
if (configResolverClass != null) {
|
if (configResolverClass != null) {
|
||||||
try {
|
try {
|
||||||
throw new RuntimeException("Not implemented yet");
|
throw new RuntimeException("Not implemented yet");
|
||||||
//KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
|
// KeycloakConfigResolver configResolver = (KeycloakConfigResolver)
|
||||||
//deploymentContext = new SamlDeploymentContext(configResolver);
|
// context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
|
||||||
//log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
|
// deploymentContext = new SamlDeploymentContext(configResolver);
|
||||||
|
// log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.",
|
||||||
|
// configResolverClass);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
|
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() });
|
||||||
//deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
// deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String fp = filterConfig.getInitParameter("keycloak.config.file");
|
String fp = filterConfig.getInitParameter("keycloak.config.file");
|
||||||
|
@ -65,7 +68,8 @@ public class SamlFilter implements Filter {
|
||||||
} else {
|
} else {
|
||||||
String path = "/WEB-INF/keycloak-saml.xml";
|
String path = "/WEB-INF/keycloak-saml.xml";
|
||||||
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
|
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
|
||||||
if (pathParam != null) path = pathParam;
|
if (pathParam != null)
|
||||||
|
path = pathParam;
|
||||||
is = filterConfig.getServletContext().getResourceAsStream(path);
|
is = filterConfig.getServletContext().getResourceAsStream(path);
|
||||||
}
|
}
|
||||||
final SamlDeployment deployment;
|
final SamlDeployment deployment;
|
||||||
|
@ -105,7 +109,6 @@ public class SamlFilter implements Filter {
|
||||||
}
|
}
|
||||||
FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper);
|
FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper);
|
||||||
|
|
||||||
|
|
||||||
SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) {
|
SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) {
|
||||||
@Override
|
@Override
|
||||||
protected void completeAuthentication(SamlSession account) {
|
protected void completeAuthentication(SamlSession account) {
|
||||||
|
@ -139,6 +142,16 @@ public class SamlFilter implements Filter {
|
||||||
challenge.challenge(facade);
|
challenge.challenge(facade);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deployment.isIsPassive() && outcome == AuthOutcome.NOT_AUTHENTICATED) {
|
||||||
|
log.fine("PASSIVE_NOT_AUTHENTICATED");
|
||||||
|
if (facade.isEnded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chain.doFilter(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!facade.isEnded()) {
|
if (!facade.isEnded()) {
|
||||||
response.sendError(403);
|
response.sendError(403);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,15 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.saml.undertow;
|
package org.keycloak.adapters.saml.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
|
import org.keycloak.adapters.saml.SamlDeploymentContext;
|
||||||
|
import org.keycloak.adapters.saml.SamlSessionStore;
|
||||||
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowHttpFacade;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
|
||||||
|
|
||||||
import io.undertow.security.api.AuthenticationMechanism;
|
import io.undertow.security.api.AuthenticationMechanism;
|
||||||
import io.undertow.security.api.NotificationReceiver;
|
import io.undertow.security.api.NotificationReceiver;
|
||||||
import io.undertow.security.api.SecurityContext;
|
import io.undertow.security.api.SecurityContext;
|
||||||
|
@ -24,14 +33,6 @@ import io.undertow.server.HttpServerExchange;
|
||||||
import io.undertow.util.AttachmentKey;
|
import io.undertow.util.AttachmentKey;
|
||||||
import io.undertow.util.Headers;
|
import io.undertow.util.Headers;
|
||||||
import io.undertow.util.StatusCodes;
|
import io.undertow.util.StatusCodes;
|
||||||
import org.keycloak.adapters.spi.AuthChallenge;
|
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
|
||||||
import org.keycloak.adapters.saml.SamlDeployment;
|
|
||||||
import org.keycloak.adapters.saml.SamlDeploymentContext;
|
|
||||||
import org.keycloak.adapters.saml.SamlSessionStore;
|
|
||||||
import org.keycloak.adapters.undertow.UndertowHttpFacade;
|
|
||||||
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
|
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
|
||||||
|
@ -44,8 +45,7 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
|
||||||
protected UndertowUserSessionManagement sessionManagement;
|
protected UndertowUserSessionManagement sessionManagement;
|
||||||
protected String errorPage;
|
protected String errorPage;
|
||||||
|
|
||||||
public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement,
|
public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) {
|
||||||
String errorPage) {
|
|
||||||
this.deploymentContext = deploymentContext;
|
this.deploymentContext = deploymentContext;
|
||||||
this.sessionManagement = sessionManagement;
|
this.sessionManagement = sessionManagement;
|
||||||
this.errorPage = errorPage;
|
this.errorPage = errorPage;
|
||||||
|
@ -69,19 +69,19 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void sendRedirect(final HttpServerExchange exchange, final String location) {
|
static void sendRedirect(final HttpServerExchange exchange, final String location) {
|
||||||
// TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this.
|
// TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better
|
||||||
|
// handle this.
|
||||||
String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location;
|
String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location;
|
||||||
exchange.getResponseHeaders().put(Headers.LOCATION, loc);
|
exchange.getResponseHeaders().put(Headers.LOCATION, loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected void registerNotifications(final SecurityContext securityContext) {
|
protected void registerNotifications(final SecurityContext securityContext) {
|
||||||
|
|
||||||
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
|
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void handleNotification(SecurityNotification notification) {
|
public void handleNotification(SecurityNotification notification) {
|
||||||
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
|
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT)
|
||||||
|
return;
|
||||||
|
|
||||||
HttpServerExchange exchange = notification.getExchange();
|
HttpServerExchange exchange = notification.getExchange();
|
||||||
UndertowHttpFacade facade = createFacade(exchange);
|
UndertowHttpFacade facade = createFacade(exchange);
|
||||||
|
@ -104,13 +104,16 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
|
||||||
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
|
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
|
||||||
}
|
}
|
||||||
SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext);
|
SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext);
|
||||||
UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade,
|
UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade, deploymentContext.resolveDeployment(facade), sessionStore);
|
||||||
deploymentContext.resolveDeployment(facade), sessionStore);
|
|
||||||
AuthOutcome outcome = authenticator.authenticate();
|
AuthOutcome outcome = authenticator.authenticate();
|
||||||
if (outcome == AuthOutcome.AUTHENTICATED) {
|
if (outcome == AuthOutcome.AUTHENTICATED) {
|
||||||
registerNotifications(securityContext);
|
registerNotifications(securityContext);
|
||||||
return AuthenticationMechanismOutcome.AUTHENTICATED;
|
return AuthenticationMechanismOutcome.AUTHENTICATED;
|
||||||
}
|
}
|
||||||
|
if (outcome == AuthOutcome.NOT_AUTHENTICATED) {
|
||||||
|
// we are in passive mode and user is not authenticated, let app server to try another auth mechanism
|
||||||
|
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
if (outcome == AuthOutcome.LOGGED_OUT) {
|
if (outcome == AuthOutcome.LOGGED_OUT) {
|
||||||
securityContext.logout();
|
securityContext.logout();
|
||||||
if (deployment.getLogoutPage() != null) {
|
if (deployment.getLogoutPage() != null) {
|
||||||
|
|
|
@ -80,4 +80,10 @@ public class StatusCodeType implements Serializable {
|
||||||
public void setValue(URI value) {
|
public void setValue(URI value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "StatusCodeType [value=" + value + ", statusCode=" + statusCode + "]";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -100,4 +100,9 @@ public class StatusType implements Serializable {
|
||||||
this.statusDetail = value;
|
this.statusDetail = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "StatusType [statusCode=" + statusCode + ", statusMessage=" + statusMessage + ", statusDetail=" + statusDetail + "]";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,16 +17,16 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.saml;
|
package org.keycloak.saml;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author pedroigor
|
* @author pedroigor
|
||||||
*/
|
*/
|
||||||
|
@ -64,6 +64,11 @@ public class SAML2AuthnRequestBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SAML2AuthnRequestBuilder isPassive(boolean isPassive) {
|
||||||
|
this.authnRequestType.setIsPassive(isPassive);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) {
|
public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) {
|
||||||
this.authnRequestType.setNameIDPolicy(nameIDPolicy.build());
|
this.authnRequestType.setNameIDPolicy(nameIDPolicy.build());
|
||||||
return this;
|
return this;
|
||||||
|
@ -74,7 +79,7 @@ public class SAML2AuthnRequestBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Document toDocument() {
|
public Document toDocument() {
|
||||||
try {
|
try {
|
||||||
AuthnRequestType authnRequestType = this.authnRequestType;
|
AuthnRequestType authnRequestType = this.authnRequestType;
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
package org.keycloak.saml;
|
package org.keycloak.saml;
|
||||||
|
|
||||||
|
<<<<<<< Upstream, based on keycloak/master
|
||||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusType;
|
import org.keycloak.dom.saml.v2.protocol.StatusType;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
=======
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
>>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
|
import org.keycloak.saml.processing.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
|
||||||
|
<<<<<<< Upstream, based on keycloak/master
|
||||||
import org.keycloak.saml.processing.core.saml.v2.holders.IDPInfoHolder;
|
import org.keycloak.saml.processing.core.saml.v2.holders.IDPInfoHolder;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.holders.IssuerInfoHolder;
|
import org.keycloak.saml.processing.core.saml.v2.holders.IssuerInfoHolder;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.holders.SPInfoHolder;
|
import org.keycloak.saml.processing.core.saml.v2.holders.SPInfoHolder;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||||
|
=======
|
||||||
|
>>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -45,7 +52,6 @@ public class SAML2ErrorResponseBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Document buildDocument() throws ProcessingException {
|
public Document buildDocument() throws ProcessingException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -65,8 +71,6 @@ public class SAML2ErrorResponseBuilder {
|
||||||
} catch (ParsingException e) {
|
} catch (ParsingException e) {
|
||||||
throw new ProcessingException(e);
|
throw new ProcessingException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,33 +147,36 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response sendError(ClientSessionModel clientSession, Error error) {
|
public Response sendError(ClientSessionModel clientSession, Error error) {
|
||||||
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
|
try {
|
||||||
session.sessions().removeClientSession(realm, clientSession);
|
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
|
||||||
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
|
if (error == Error.CANCELLED_BY_USER) {
|
||||||
if (error == Error.CANCELLED_BY_USER) {
|
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
|
||||||
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
|
Map<String, String> params = new HashMap<>();
|
||||||
Map<String, String> params = new HashMap<>();
|
params.put("realm", realm.getName());
|
||||||
params.put("realm", realm.getName());
|
params.put("protocol", LOGIN_PROTOCOL);
|
||||||
params.put("protocol", LOGIN_PROTOCOL);
|
params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
|
||||||
params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
|
URI redirect = builder.buildFromMap(params);
|
||||||
URI redirect = builder.buildFromMap(params);
|
return Response.status(302).location(redirect).build();
|
||||||
return Response.status(302).location(redirect).build();
|
|
||||||
} else {
|
|
||||||
return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
|
|
||||||
try {
|
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
|
|
||||||
Document document = builder.buildDocument();
|
|
||||||
if (isPostBinding(clientSession)) {
|
|
||||||
return binding.postBinding(document).response(clientSession.getRedirectUri());
|
|
||||||
} else {
|
} else {
|
||||||
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
|
return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
|
||||||
|
try {
|
||||||
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
|
||||||
|
Document document = builder.buildDocument();
|
||||||
|
if (isPostBinding(clientSession)) {
|
||||||
|
return binding.postBinding(document).response(clientSession.getRedirectUri());
|
||||||
|
} else {
|
||||||
|
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
|
||||||
|
session.sessions().removeClientSession(realm, clientSession);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ public class SamlAdapterTest {
|
||||||
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
|
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
|
||||||
|
|
||||||
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
|
||||||
|
initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader);
|
||||||
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
|
||||||
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
|
||||||
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
|
||||||
|
@ -96,6 +97,11 @@ public class SamlAdapterTest {
|
||||||
testStrategy.testPostSimpleLoginLogout();
|
testStrategy.testPostSimpleLoginLogout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostPassiveLoginLogout() {
|
||||||
|
testStrategy.testPostPassiveLoginLogout(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostSignedLoginLogoutTransientNameID() {
|
public void testPostSignedLoginLogoutTransientNameID() {
|
||||||
testStrategy.testPostSignedLoginLogoutTransientNameID();
|
testStrategy.testPostSignedLoginLogoutTransientNameID();
|
||||||
|
|
|
@ -139,6 +139,37 @@ public class SamlAdapterTestStrategy extends ExternalResource {
|
||||||
checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post/");
|
checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testPostPassiveLoginLogout(boolean forbiddenIfNotauthenticated) {
|
||||||
|
// first request on passive app - no login page shown, user not logged in as we are in passive mode
|
||||||
|
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
|
||||||
|
assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
|
||||||
|
System.out.println(driver.getPageSource());
|
||||||
|
if (forbiddenIfNotauthenticated) {
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
|
||||||
|
} else {
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("principal=null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// login user by asking login from other app
|
||||||
|
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
|
||||||
|
loginPage.login("bburke", "password");
|
||||||
|
|
||||||
|
// navigate to the passive app again, we have to be logged in now
|
||||||
|
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
|
||||||
|
assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
|
||||||
|
System.out.println(driver.getPageSource());
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("bburke"));
|
||||||
|
|
||||||
|
// logout from both app
|
||||||
|
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive?GLO=true");
|
||||||
|
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true");
|
||||||
|
|
||||||
|
// refresh passive app page, not logged in again as we are in passive mode
|
||||||
|
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
|
||||||
|
assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
|
||||||
|
Assert.assertFalse(driver.getPageSource().contains("bburke"));
|
||||||
|
}
|
||||||
|
|
||||||
public void testPostSimpleUnauthorized(CheckAuthError error) {
|
public void testPostSimpleUnauthorized(CheckAuthError error) {
|
||||||
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
|
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
|
||||||
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
|
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
|
||||||
|
|
|
@ -1,35 +1,24 @@
|
||||||
package org.keycloak.testsuite.saml;
|
package org.keycloak.testsuite.saml;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.Config;
|
|
||||||
import org.keycloak.admin.client.Keycloak;
|
import org.keycloak.admin.client.Keycloak;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.models.UserSessionModel;
|
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
|
||||||
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
|
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
|
||||||
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
|
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
|
||||||
import org.keycloak.protocol.saml.mappers.HardcodedRole;
|
import org.keycloak.protocol.saml.mappers.HardcodedRole;
|
||||||
import org.keycloak.protocol.saml.mappers.RoleListMapper;
|
import org.keycloak.protocol.saml.mappers.RoleListMapper;
|
||||||
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
|
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.resources.admin.AdminRoot;
|
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
import org.keycloak.testsuite.rule.WebResource;
|
import org.keycloak.testsuite.rule.WebResource;
|
||||||
import org.keycloak.testsuite.rule.WebRule;
|
import org.keycloak.testsuite.rule.WebRule;
|
||||||
|
@ -47,19 +36,10 @@ import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.ws.rs.client.Client;
|
|
||||||
import javax.ws.rs.client.ClientBuilder;
|
|
||||||
import javax.ws.rs.client.ClientRequestContext;
|
|
||||||
import javax.ws.rs.client.ClientRequestFilter;
|
|
||||||
import javax.ws.rs.client.Entity;
|
|
||||||
import javax.ws.rs.client.WebTarget;
|
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
|
||||||
import javax.ws.rs.core.MediaType;
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@ -166,6 +146,7 @@ public class SamlBindingTest {
|
||||||
driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
|
driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
|
||||||
checkLoggedOut("http://localhost:8081/sales-post/");
|
checkLoggedOut("http://localhost:8081/sales-post/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostSimpleLoginLogoutIdpInitiated() {
|
public void testPostSimpleLoginLogoutIdpInitiated() {
|
||||||
driver.navigate().to("http://localhost:8081/auth/realms/demo/protocol/saml/clients/sales-post");
|
driver.navigate().to("http://localhost:8081/auth/realms/demo/protocol/saml/clients/sales-post");
|
||||||
|
@ -188,6 +169,7 @@ public class SamlBindingTest {
|
||||||
checkLoggedOut("http://localhost:8081/sales-post-sig/");
|
checkLoggedOut("http://localhost:8081/sales-post-sig/");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostSignedLoginLogoutTransientNameID() {
|
public void testPostSignedLoginLogoutTransientNameID() {
|
||||||
driver.navigate().to("http://localhost:8081/sales-post-sig-transient/");
|
driver.navigate().to("http://localhost:8081/sales-post-sig-transient/");
|
||||||
|
@ -452,23 +434,10 @@ public class SamlBindingTest {
|
||||||
Assert.assertTrue(driver.getPageSource().contains("null"));
|
Assert.assertTrue(driver.getPageSource().contains("null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String createToken() {
|
@Test
|
||||||
KeycloakSession session = keycloakRule.startSession();
|
public void testPassiveMode() {
|
||||||
try {
|
// KEYCLOAK-2075 test SAML IsPassive handling - PicketLink SP client library doesn't support this option unfortunately.
|
||||||
RealmManager manager = new RealmManager(session);
|
// But the test of server side is included in test of SAML Keycloak adapter
|
||||||
|
|
||||||
RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
|
|
||||||
ClientModel adminConsole = adminRealm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
|
|
||||||
TokenManager tm = new TokenManager();
|
|
||||||
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
|
|
||||||
ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
|
|
||||||
clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master");
|
|
||||||
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
|
|
||||||
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
|
|
||||||
return tm.encodeToken(adminRealm, token);
|
|
||||||
} finally {
|
|
||||||
keycloakRule.stopSession(session, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import org.junit.ClassRule;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy;
|
import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy;
|
||||||
import org.keycloak.testsuite.keycloaksaml.SamlSPFacade;
|
|
||||||
import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet;
|
import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
@ -25,6 +24,7 @@ public class SamlAdapterTest {
|
||||||
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
|
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
|
||||||
|
|
||||||
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
|
||||||
|
initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader);
|
||||||
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
|
||||||
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
|
||||||
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
|
||||||
|
@ -37,9 +37,6 @@ public class SamlAdapterTest {
|
||||||
initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader);
|
||||||
initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
|
initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
|
||||||
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
|
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -105,6 +102,11 @@ public class SamlAdapterTest {
|
||||||
testStrategy.testPostSimpleLoginLogout();
|
testStrategy.testPostSimpleLoginLogout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostPassiveLoginLogout() {
|
||||||
|
testStrategy.testPostPassiveLoginLogout(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostSignedLoginLogoutTransientNameID() {
|
public void testPostSignedLoginLogoutTransientNameID() {
|
||||||
testStrategy.testPostSignedLoginLogoutTransientNameID();
|
testStrategy.testPostSignedLoginLogoutTransientNameID();
|
||||||
|
|
|
@ -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"
|
"saml_idp_initiated_sso_url_name": "sales-post"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "http://localhost:8081/sales-post-passive/",
|
||||||
|
"enabled": true,
|
||||||
|
"fullScopeAllowed": true,
|
||||||
|
"protocol": "saml",
|
||||||
|
"baseUrl": "http://localhost:8081/sales-post-passive",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8081/sales-post-passive/*"
|
||||||
|
],
|
||||||
|
"attributes": {
|
||||||
|
"saml.authnstatement": "true",
|
||||||
|
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-passive/",
|
||||||
|
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-passive/",
|
||||||
|
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post-passive/",
|
||||||
|
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-passive/",
|
||||||
|
"saml_idp_initiated_sso_url_name": "sales-post-passive"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "http://localhost:8081/sales-post-sig/",
|
"name": "http://localhost:8081/sales-post-sig/",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
Loading…
Reference in a new issue