[KEYCLOAK-8104] Keycloak SAML Adapter does not support clockSkew configuration

Co-Authored-By: vramik <vramik@redhat.com>
This commit is contained in:
Steeve Beroard 2019-05-17 18:14:16 +02:00 committed by Hynek Mlnařík
parent 1d2d6591b2
commit fc9a0e1766
25 changed files with 1159 additions and 50 deletions

View file

@ -205,6 +205,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
private SingleLogoutService singleLogoutService; private SingleLogoutService singleLogoutService;
private final List<PublicKey> signatureValidationKeys = new LinkedList<>(); private final List<PublicKey> signatureValidationKeys = new LinkedList<>();
private int minTimeBetweenDescriptorRequests; private int minTimeBetweenDescriptorRequests;
private int allowedClockSkew;
private HttpClient client; private HttpClient client;
private String metadataUrl; private String metadataUrl;
@ -284,6 +285,16 @@ public class DefaultSamlDeployment implements SamlDeployment {
public void setMetadataUrl(String metadataUrl) { public void setMetadataUrl(String metadataUrl) {
this.metadataUrl = metadataUrl; this.metadataUrl = metadataUrl;
} }
@Override
public int getAllowedClockSkew() {
return allowedClockSkew;
}
public void setAllowedClockSkew(int allowedClockSkew) {
this.allowedClockSkew = allowedClockSkew;
}
} }
private IDP idp; private IDP idp;

View file

@ -85,6 +85,12 @@ public interface SamlDeployment {
*/ */
HttpClient getClient(); HttpClient getClient();
/**
* Returns allowed time difference (in milliseconds) between IdP and SP
* @return see description
*/
int getAllowedClockSkew();
public interface SingleSignOnService { public interface SingleSignOnService {
/** /**
* Returns {@code true} if the requests to IdP need to be signed by SP key. * Returns {@code true} if the requests to IdP need to be signed by SP key.

View file

@ -19,6 +19,8 @@ package org.keycloak.adapters.saml.config;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import org.keycloak.adapters.cloned.AdapterHttpClientConfig; import org.keycloak.adapters.cloned.AdapterHttpClientConfig;
/** /**
@ -270,6 +272,8 @@ public class IDP implements Serializable {
private AdapterHttpClientConfig httpClientConfig = new HttpClientConfig(); private AdapterHttpClientConfig httpClientConfig = new HttpClientConfig();
private boolean signaturesRequired = false; private boolean signaturesRequired = false;
private String metadataUrl; private String metadataUrl;
private Integer allowedClockSkew;
private TimeUnit allowedClockSkewUnit;
public String getEntityID() { public String getEntityID() {
return entityID; return entityID;
@ -348,4 +352,20 @@ public class IDP implements Serializable {
public void setMetadataUrl(String metadataUrl) { public void setMetadataUrl(String metadataUrl) {
this.metadataUrl = metadataUrl; this.metadataUrl = metadataUrl;
} }
public Integer getAllowedClockSkew() {
return allowedClockSkew;
}
public void setAllowedClockSkew(Integer allowedClockSkew) {
this.allowedClockSkew = allowedClockSkew;
}
public TimeUnit getAllowedClockSkewUnit() {
return allowedClockSkewUnit;
}
public void setAllowedClockSkewUnit(TimeUnit allowedClockSkewUnit) {
this.allowedClockSkewUnit = allowedClockSkewUnit;
}
} }

View file

@ -44,10 +44,8 @@ import java.util.Set;
import org.keycloak.adapters.cloned.HttpClientBuilder; import org.keycloak.adapters.cloned.HttpClientBuilder;
import java.net.URI; import java.net.URI;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.logging.Level; import java.util.concurrent.TimeUnit;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -172,6 +170,9 @@ public class DeploymentBuilder {
sso.setResponseBinding(SamlDeployment.Binding.parseBinding( sso.setResponseBinding(SamlDeployment.Binding.parseBinding(
idp.getSingleSignOnService().getResponseBinding())); idp.getSingleSignOnService().getResponseBinding()));
} }
if (idp.getAllowedClockSkew() != null) {
defaultIDP.setAllowedClockSkew(convertClockSkewInMillis(idp.getAllowedClockSkew(), idp.getAllowedClockSkewUnit()));
}
if (idp.getSingleSignOnService().getAssertionConsumerServiceUrl() != null) { if (idp.getSingleSignOnService().getAssertionConsumerServiceUrl() != null) {
if (! idp.getSingleSignOnService().getAssertionConsumerServiceUrl().endsWith("/saml")) { if (! idp.getSingleSignOnService().getAssertionConsumerServiceUrl().endsWith("/saml")) {
throw new RuntimeException("AssertionConsumerServiceUrl must end with \"/saml\"."); throw new RuntimeException("AssertionConsumerServiceUrl must end with \"/saml\".");
@ -214,6 +215,18 @@ public class DeploymentBuilder {
return deployment; return deployment;
} }
private int convertClockSkewInMillis(int duration, TimeUnit unit) {
int durationMillis = (int) unit.toMillis(duration);
switch (unit) {
case NANOSECONDS:
case MICROSECONDS:
log.warn("Clock skew value will be rounded down.");
default:
log.info("Clock skew set to " + durationMillis + "ms.");
}
return durationMillis;
}
private void processSigningKey(DefaultSamlDeployment.DefaultIDP idp, Key key, ResourceLoader resourceLoader) throws RuntimeException { private void processSigningKey(DefaultSamlDeployment.DefaultIDP idp, Key key, ResourceLoader resourceLoader) throws RuntimeException {
PublicKey publicKey; PublicKey publicKey;
if (key.getKeystore() != null) { if (key.getKeystore() != null) {

View file

@ -23,6 +23,7 @@ import org.keycloak.saml.common.util.StaxParserUtil;
import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.StartElement; import javax.xml.stream.events.StartElement;
import java.util.concurrent.TimeUnit;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -72,6 +73,13 @@ public class IdpParser extends AbstractKeycloakSamlAdapterV1Parser<IDP> {
case SINGLE_LOGOUT_SERVICE: case SINGLE_LOGOUT_SERVICE:
target.setSingleLogoutService(SingleLogoutServiceParser.getInstance().parse(xmlEventReader)); target.setSingleLogoutService(SingleLogoutServiceParser.getInstance().parse(xmlEventReader));
break; break;
case ALLOWED_CLOCK_SKEW:
String timeUnitString = StaxParserUtil.getAttributeValueRP(elementDetail, KeycloakSamlAdapterV1QNames.ATTR_UNIT);
target.setAllowedClockSkewUnit(timeUnitString == null ? TimeUnit.SECONDS : TimeUnit.valueOf(timeUnitString));
StaxParserUtil.advance(xmlEventReader);
target.setAllowedClockSkew(Integer.parseInt(StaxParserUtil.getElementText(xmlEventReader)));
break;
} }
} }
} }

View file

@ -25,6 +25,7 @@ import javax.xml.namespace.QName;
*/ */
public enum KeycloakSamlAdapterV1QNames implements HasQName { public enum KeycloakSamlAdapterV1QNames implements HasQName {
ALLOWED_CLOCK_SKEW("AllowedClockSkew"),
ATTRIBUTE("Attribute"), ATTRIBUTE("Attribute"),
CERTIFICATE("Certificate"), CERTIFICATE("Certificate"),
CERTIFICATE_PEM("CertificatePem"), CERTIFICATE_PEM("CertificatePem"),
@ -81,6 +82,7 @@ public enum KeycloakSamlAdapterV1QNames implements HasQName {
ATTR_TRUSTSTORE_PASSWORD(null, "truststorePassword"), ATTR_TRUSTSTORE_PASSWORD(null, "truststorePassword"),
ATTR_TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN(null, "turnOffChangeSessionIdOnLogin"), ATTR_TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN(null, "turnOffChangeSessionIdOnLogin"),
ATTR_TYPE(null, "type"), ATTR_TYPE(null, "type"),
ATTR_UNIT(null, "unit"),
ATTR_VALIDATE_ASSERTION_SIGNATURE(null, "validateAssertionSignature"), ATTR_VALIDATE_ASSERTION_SIGNATURE(null, "validateAssertionSignature"),
ATTR_VALIDATE_REQUEST_SIGNATURE(null, "validateRequestSignature"), ATTR_VALIDATE_REQUEST_SIGNATURE(null, "validateRequestSignature"),
ATTR_VALIDATE_RESPONSE_SIGNATURE(null, "validateResponseSignature"), ATTR_VALIDATE_RESPONSE_SIGNATURE(null, "validateResponseSignature"),

View file

@ -345,6 +345,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
assertion = AssertionUtil.getAssertion(responseHolder, responseType, deployment.getDecryptionKey()); assertion = AssertionUtil.getAssertion(responseHolder, responseType, deployment.getDecryptionKey());
ConditionsValidator.Builder cvb = new ConditionsValidator.Builder(assertion.getID(), assertion.getConditions(), destinationValidator); ConditionsValidator.Builder cvb = new ConditionsValidator.Builder(assertion.getID(), assertion.getConditions(), destinationValidator);
try { try {
cvb.clockSkewInMillis(deployment.getIDP().getAllowedClockSkew());
cvb.addAllowedAudience(URI.create(deployment.getEntityID())); cvb.addAllowedAudience(URI.create(deployment.getEntityID()));
// getDestination has been validated to match request URL already so it matches SAML endpoint // getDestination has been validated to match request URL already so it matches SAML endpoint
cvb.addAllowedAudience(URI.create(responseType.getDestination())); cvb.addAllowedAudience(URI.create(responseType.getDestination()));

View file

@ -0,0 +1,492 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<xs:schema version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="urn:keycloak:saml:adapter"
targetNamespace="urn:keycloak:saml:adapter"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="keycloak-saml-adapter" type="adapter-type"/>
<xs:complexType name="adapter-type">
<xs:annotation>
<xs:documentation>Keycloak SAML Adapter configuration file.</xs:documentation>
</xs:annotation>
<xs:all>
<xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type">
<xs:annotation>
<xs:documentation>Describes SAML service provider configuration.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
</xs:complexType>
<xs:complexType name="sp-type">
<xs:all>
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
List of service provider encryption and validation keys.
If the IDP requires that the client application (SP) sign all of its requests and/or if the IDP will encrypt assertions, you must define the keys used to do this. For client signed documents you must define both the private and public key or certificate that will be used to sign documents. For encryption, you only have to define the private key that will be used to decrypt.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>When creating a Java Principal object that you obtain from methods like HttpServletRequest.getUserPrincipal(), you can define what name that is returned by the Principal.getName() method.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Defines what SAML attributes within the assertion received from the user should be used as role identifiers within the Java EE Security Context for the user.
By default Role attribute values are converted to Java EE roles. Some IDPs send roles via a member or memberOf attribute assertion. You can define one or more Attribute elements to specify which SAML attributes must be converted into roles.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation>Describes configuration of SAML identity provider for this service provider.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>This is the identifier for this client. The IDP needs this value to determine who the client is that is communicating with it.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="sslPolicy" type="ssl-policy-type" use="optional">
<xs:annotation>
<xs:documentation>SSL policy the adapter will enforce.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>SAML clients can request a specific NameID Subject format. Fill in this value if you want a specific format. It must be a standard SAML format identifier, i.e. urn:oasis:names:tc:SAML:2.0:nameid-format:transient. By default, no special format is requested.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="logoutPage" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>URL of the logout page.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>SAML clients can request that a user is re-authenticated even if they are already logged in at the IDP. Default value is false.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>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 true if you want this. Do not use together with forceAuthentication as they are opposite. Default value is false.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>The session id is changed by default on a successful login on some platforms to plug a security attack vector. Change this to true to disable this. It is recommended you do not turn it off. Default value is false.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="autodetectBearerOnly" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>This should be set to true if your application serves both a web application and web services (e.g. SOAP or REST). It allows you to redirect unauthenticated users of the web application to the Keycloak login page, but send an HTTP 401 status code to unauthenticated SOAP or REST clients instead as they would not understand a redirect to the login page. Keycloak auto-detects SOAP or REST clients based on typical headers like X-Requested-With, SOAPAction or Accept. The default value is false.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="keys-type">
<xs:sequence>
<xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Describes a single key used for signing or encryption.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="key-type">
<xs:all>
<xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type">
<xs:annotation>
<xs:documentation>Java keystore to load keys and certificates from.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Private key (PEM format)</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Public key (PEM format)</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Certificate key (PEM format)</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="signing" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Flag defining whether the key should be used for signing.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encryption" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Flag defining whether the key should be used for encryption</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="key-store-type">
<xs:all>
<xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type">
<xs:annotation>
<xs:documentation>Private key declaration</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Certificate declaration</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="file" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>File path to the key store.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="resource" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>WAR resource path to the key store. This is a path used in method call to ServletContext.getResourceAsStream().</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The password of the key store.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="private-key-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Keystores require an additional password to access private keys. In the PrivateKey element you must define this password within a password attribute.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="certificate-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="principal-name-mapping-type">
<xs:attribute name="policy" type="principal-name-mapping-policy-type" use="required">
<xs:annotation>
<xs:documentation>Policy used to populate value of Java Principal object obtained from methods like HttpServletRequest.getUserPrincipal().</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="attribute" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Name of the SAML assertion attribute to use within.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:simpleType name="principal-name-mapping-policy-type">
<xs:restriction base="xs:string">
<xs:enumeration value="FROM_NAME_ID">
<xs:annotation>
<xs:documentation>This policy just uses whatever the SAML subject value is. This is the default setting</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="FROM_ATTRIBUTE">
<xs:annotation>
<xs:documentation>This will pull the value from one of the attributes declared in the SAML assertion received from the server. You'll need to specify the name of the SAML assertion attribute to use within the attribute XML attribute.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ssl-policy-type">
<xs:restriction base="xs:string">
<xs:enumeration value="ALL">
<xs:annotation>
<xs:documentation>All requests must come in via HTTPS.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="EXTERNAL">
<xs:annotation>
<xs:documentation>Only non-private IP addresses must come over the wire via HTTPS.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="NONE">
<xs:annotation>
<xs:documentation>no requests are required to come over via HTTPS.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="signature-algorithm-type">
<xs:restriction base="xs:string">
<xs:enumeration value="RSA_SHA1"/>
<xs:enumeration value="RSA_SHA256"/>
<xs:enumeration value="RSA_SHA512"/>
<xs:enumeration value="DSA_SHA1"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="binding-type">
<xs:restriction base="xs:string">
<xs:enumeration value="POST"/>
<xs:enumeration value="REDIRECT"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="role-identifiers-type">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type">
<xs:annotation>
<xs:documentation>Specifies SAML attribute to be converted into roles.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
<xs:complexType name="attribute-type">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Specifies name of the SAML attribute to be converted into roles.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="idp-type">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type">
<xs:annotation>
<xs:documentation>Configuration of the login SAML endpoint of the IDP.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Configuration of the logout SAML endpoint of the IDP</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The Keys sub element of IDP is only used to define the certificate or public key to use to verify documents signed by the IDP.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Configuration of HTTP client used for automatic obtaining of certificates containing public keys for IDP signature verification via SAML descriptor of the IDP.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="AllowedClockSkew" type="allowed-clock-skew-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>This defines the allowed clock skew between IDP and SP in milliseconds. The default value is 0.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>issuer ID of the IDP.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signaturesRequired" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>If set to true, the client adapter will sign every document it sends to the IDP. Also, the client will expect that the IDP will be signing any documents sent to it. This switch sets the default for all request and response types.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureAlgorithm" type="signature-algorithm-type" use="optional">
<xs:annotation>
<xs:documentation>Signature algorithm that the IDP expects signed documents to use. Defaults to RSA_SHA256</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>This is the signature canonicalization method that the IDP expects signed documents to use. The default value is https://www.w3.org/2001/10/xml-exc-c14n# and should be good for most IDPs.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encryption" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation></xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="metadataUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The URL used to retrieve the IDP metadata, currently this is only used to pick up signing and encryption keys periodically which allow cycling of these keys on the IDP without manual changes on the SP side.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="sign-on-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client expect the IDP to sign the assertion response document sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client expect the IDP to sign the individual assertions sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>SAML binding type used for communicating with the IDP. The default value is POST, but you can set it to REDIRECT as well.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>SAML allows the client to request what binding type it wants authn responses to use. This value maps to ProtocolBinding attribute in SAML AuthnRequest. The default is that the client will not request a specific binding type for responses.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="bindingUrl" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>This is the URL for the IDP login service that the client will send requests to.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="assertionConsumerServiceUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>URL of the assertion consumer service (ACS) where the IDP login service should send responses to. By default it is unset, relying on the IdP settings. When set, it must end in "/saml". This property is typically accompanied by the responseBinding attribute.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="logout-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signResponse" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client sign logout responses it sends to the IDP requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client expect signed logout request documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client expect signed logout response documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>This is the SAML binding type used for communicating SAML requests to the IDP. The default value is POST.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>This is the SAML binding type used for communicating SAML responses to the IDP. The default value is POST.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="postBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>This is the URL for the IDP's logout service when using the POST binding. This setting is REQUIRED if using the POST binding.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>This is the URL for the IDP's logout service when using the REDIRECT binding. This setting is REQUIRED if using the REDIRECT binding.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="http-client-type">
<xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>If the the IDP server requires HTTPS and this config option is set to true the IDP's certificate
is validated via the truststore, but host name validation is not done. This setting should only be used during
development and never in production as it will partly disable verification of SSL certificates.
This seting may be useful in test environments. The default value is false.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="clientKeystore" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>This is the file path to a keystore file. This keystore contains client certificate
for two-way SSL when the adapter makes HTTPS requests to the IDP server.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="clientKeystorePassword" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Password for the client keystore and for the client's key.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="connectionPoolSize" type="xs:int" use="optional" default="10">
<xs:annotation>
<xs:documentation>Defines number of pooled connections.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="disableTrustManager" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>If the the IDP server requires HTTPS and this config option is set to true you do not have to specify a truststore.
This setting should only be used during development and never in production as it will disable verification of SSL certificates.
The default value is false.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="proxyUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>URL to HTTP proxy to use for HTTP connections.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="truststore" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The value is the file path to a keystore file. If you prefix the path with classpath:,
then the truststore will be obtained from the deployment's classpath instead. Used for outgoing
HTTPS communications to the IDP server. Client making HTTPS requests need
a way to verify the host of the server they are talking to. This is what the trustore does.
The keystore contains one or more trusted host certificates or certificate authorities.
You can create this truststore by extracting the public certificate of the IDP's SSL keystore.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="truststorePassword" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Password for the truststore keystore.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="allowed-clock-skew-type">
<xs:annotation>
<xs:documentation>The value is the allowed clock skew between the IDP and the SP.</xs:documentation>
</xs:annotation>
<xs:simpleContent>
<xs:extension base="xs:positiveInteger">
<xs:attribute name="unit" type="clock-skew-unit-type"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="clock-skew-unit-type">
<xs:annotation>
<xs:documentation>Time unit for the value of the clock skew.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:enumeration value="MINUTES" />
<xs:enumeration value="SECONDS" />
<xs:enumeration value="MILLISECONDS" />
<xs:enumeration value="MICROSECONDS" />
<xs:enumeration value="NANOSECONDS" />
</xs:restriction>
</xs:simpleType>
</xs:schema>

View file

@ -31,6 +31,8 @@ import org.junit.Rule;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ParsingException;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
/** /**
@ -39,7 +41,7 @@ import org.hamcrest.Matchers;
*/ */
public class KeycloakSamlAdapterXMLParserTest { public class KeycloakSamlAdapterXMLParserTest {
private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_10.xsd"; private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_11.xsd";
@Rule @Rule
public ExpectedException expectedException = ExpectedException.none(); public ExpectedException expectedException = ExpectedException.none();
@ -72,6 +74,11 @@ public class KeycloakSamlAdapterXMLParserTest {
testValidationValid("keycloak-saml-with-metadata-url.xml"); testValidationValid("keycloak-saml-with-metadata-url.xml");
} }
@Test
public void testValidationWithAllowedClockSkew() throws Exception {
testValidationValid("keycloak-saml-with-allowed-clock-skew-with-unit.xml");
}
@Test @Test
public void testValidationKeyInvalid() throws Exception { public void testValidationKeyInvalid() throws Exception {
InputStream schemaIs = KeycloakSamlAdapterV1Parser.class.getResourceAsStream(CURRENT_XSD_LOCATION); InputStream schemaIs = KeycloakSamlAdapterV1Parser.class.getResourceAsStream(CURRENT_XSD_LOCATION);
@ -258,4 +265,26 @@ public class KeycloakSamlAdapterXMLParserTest {
IDP idp = sp.getIdp(); IDP idp = sp.getIdp();
assertThat(idp.getMetadataUrl(), is("https:///example.com/metadata.xml")); assertThat(idp.getMetadataUrl(), is("https:///example.com/metadata.xml"));
} }
@Test
public void testAllowedClockSkewDefaultUnit() throws Exception {
KeycloakSamlAdapter config = parseKeycloakSamlAdapterConfig("keycloak-saml-with-allowed-clock-skew-default-unit.xml", KeycloakSamlAdapter.class);
assertNotNull(config);
assertThat(config.getSps(), Matchers.contains(instanceOf(SP.class)));
SP sp = config.getSps().get(0);
IDP idp = sp.getIdp();
assertThat(idp.getAllowedClockSkew(), is(3));
assertThat(idp.getAllowedClockSkewUnit(), is(TimeUnit.SECONDS));
}
@Test
public void testAllowedClockSkewWithUnit() throws Exception {
KeycloakSamlAdapter config = parseKeycloakSamlAdapterConfig("keycloak-saml-with-allowed-clock-skew-with-unit.xml", KeycloakSamlAdapter.class);
assertNotNull(config);
assertThat(config.getSps(), Matchers.contains(instanceOf(SP.class)));
SP sp = config.getSps().get(0);
IDP idp = sp.getIdp();
assertThat(idp.getAllowedClockSkew(), is(3500));
assertThat(idp.getAllowedClockSkewUnit(), is (TimeUnit.MILLISECONDS));
}
} }

View file

@ -0,0 +1,78 @@
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_11.xsd">
<SP entityID="sp"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="format"
forceAuthentication="true"
isPassive="true">
<Keys>
<Key signing="true">
<KeyStore file="file" resource="cp" password="pw">
<PrivateKey alias="private alias" password="private pw"/>
<Certificate alias="cert alias"/>
</KeyStore>
</Key>
<Key encryption="true">
<PrivateKeyPem>
private pem
</PrivateKeyPem>
<PublicKeyPem>
public pem
</PublicKeyPem>
</Key>
</Keys>
<PrincipalNameMapping policy="FROM_ATTRIBUTE" attribute="attribute"/>
<RoleIdentifiers>
<Attribute name="member"/>
</RoleIdentifiers>
<IDP entityID="idp"
signatureAlgorithm="RSA_SHA256"
signatureCanonicalizationMethod="canon"
signaturesRequired="true"
metadataUrl="https:///example.com/metadata.xml"
>
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
requestBinding="POST"
bindingUrl="url"
/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="false"
signResponse="true"
requestBinding="REDIRECT"
responseBinding="POST"
postBindingUrl="posturl"
redirectBindingUrl="redirecturl"
/>
<Keys>
<Key signing="true">
<CertificatePem>
cert pem
</CertificatePem>
</Key>
</Keys>
<AllowedClockSkew>3</AllowedClockSkew> <!-- 3 seconds -->
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -0,0 +1,78 @@
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_11.xsd">
<SP entityID="sp"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="format"
forceAuthentication="true"
isPassive="true">
<Keys>
<Key signing="true">
<KeyStore file="file" resource="cp" password="pw">
<PrivateKey alias="private alias" password="private pw"/>
<Certificate alias="cert alias"/>
</KeyStore>
</Key>
<Key encryption="true">
<PrivateKeyPem>
private pem
</PrivateKeyPem>
<PublicKeyPem>
public pem
</PublicKeyPem>
</Key>
</Keys>
<PrincipalNameMapping policy="FROM_ATTRIBUTE" attribute="attribute"/>
<RoleIdentifiers>
<Attribute name="member"/>
</RoleIdentifiers>
<IDP entityID="idp"
signatureAlgorithm="RSA_SHA256"
signatureCanonicalizationMethod="canon"
signaturesRequired="true"
metadataUrl="https:///example.com/metadata.xml"
>
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
requestBinding="POST"
bindingUrl="url"
/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="false"
signResponse="true"
requestBinding="REDIRECT"
responseBinding="POST"
postBindingUrl="posturl"
redirectBindingUrl="redirecturl"
/>
<Keys>
<Key signing="true">
<CertificatePem>
cert pem
</CertificatePem>
</Key>
</Keys>
<AllowedClockSkew unit="MILLISECONDS">3500</AllowedClockSkew> <!-- 3.5 seconds -->
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -19,10 +19,9 @@ package org.keycloak.saml.processing.core.saml.v2.util;
import org.keycloak.saml.common.PicketLinkLogger; import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory; import org.keycloak.saml.common.PicketLinkLoggerFactory;
import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.util.SecurityActions; import org.keycloak.saml.common.util.SecurityActions;
import org.keycloak.saml.common.util.SystemPropertiesUtil; import org.keycloak.saml.common.util.SystemPropertiesUtil;
import org.keycloak.common.util.Time;
import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeConstants; import javax.xml.datatype.DatatypeConstants;
@ -31,6 +30,7 @@ import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.datatype.XMLGregorianCalendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
/** /**
* Util class dealing with xml based time * Util class dealing with xml based time
@ -49,8 +49,6 @@ public class XMLTimeUtil {
* @param millis * @param millis
* *
* @return calendar value with the addition * @return calendar value with the addition
*
* @throws org.keycloak.saml.common.exceptions.ConfigurationException
*/ */
public static XMLGregorianCalendar add(XMLGregorianCalendar value, long millis) { public static XMLGregorianCalendar add(XMLGregorianCalendar value, long millis) {
if (value == null) { if (value == null) {
@ -76,8 +74,6 @@ public class XMLTimeUtil {
* @param millis miliseconds entered in a positive value * @param millis miliseconds entered in a positive value
* *
* @return * @return
*
* @throws ConfigurationException
*/ */
public static XMLGregorianCalendar subtract(XMLGregorianCalendar value, long millis) { public static XMLGregorianCalendar subtract(XMLGregorianCalendar value, long millis) {
return add(value, - millis); return add(value, - millis);
@ -91,8 +87,6 @@ public class XMLTimeUtil {
* @param timezone * @param timezone
* *
* @return * @return
*
* @throws ConfigurationException
*/ */
public static XMLGregorianCalendar getIssueInstant(String timezone) { public static XMLGregorianCalendar getIssueInstant(String timezone) {
TimeZone tz = TimeZone.getTimeZone(timezone); TimeZone tz = TimeZone.getTimeZone(timezone);
@ -102,6 +96,12 @@ public class XMLTimeUtil {
GregorianCalendar gc = new GregorianCalendar(tz); GregorianCalendar gc = new GregorianCalendar(tz);
XMLGregorianCalendar xgc = dtf.newXMLGregorianCalendar(gc); XMLGregorianCalendar xgc = dtf.newXMLGregorianCalendar(gc);
Long offsetMilis = TimeUnit.MILLISECONDS.convert(Time.getOffset(), TimeUnit.SECONDS);
if (offsetMilis != 0) {
if (logger.isDebugEnabled()) logger.debug(XMLTimeUtil.class.getName() + " timeOffset: " + offsetMilis);
xgc.add(parseAsDuration(offsetMilis.toString()));
}
if (logger.isDebugEnabled()) logger.debug(XMLTimeUtil.class.getName() + " issueInstant: " + xgc.toString());
return xgc; return xgc;
} }
@ -109,8 +109,6 @@ public class XMLTimeUtil {
* Get the current instant of time * Get the current instant of time
* *
* @return * @return
*
* @throws ConfigurationException
*/ */
public static XMLGregorianCalendar getIssueInstant() { public static XMLGregorianCalendar getIssueInstant() {
return getIssueInstant(getCurrentTimeZoneID()); return getIssueInstant(getCurrentTimeZoneID());
@ -179,10 +177,8 @@ public class XMLTimeUtil {
* @param timeValue * @param timeValue
* *
* @return * @return
*
* @throws org.keycloak.saml.common.exceptions.ParsingException
*/ */
public static Duration parseAsDuration(String timeValue) throws ParsingException { public static Duration parseAsDuration(String timeValue) {
if (timeValue == null) { if (timeValue == null) {
PicketLinkLoggerFactory.getLogger().nullArgumentError("duration time"); PicketLinkLoggerFactory.getLogger().nullArgumentError("duration time");
} }
@ -207,10 +203,8 @@ public class XMLTimeUtil {
* @param timeString * @param timeString
* *
* @return * @return
*
* @throws ParsingException
*/ */
public static XMLGregorianCalendar parse(String timeString) throws ParsingException { public static XMLGregorianCalendar parse(String timeString) {
DatatypeFactory factory = DATATYPE_FACTORY.get(); DatatypeFactory factory = DATATYPE_FACTORY.get();
return factory.newXMLGregorianCalendar(timeString); return factory.newXMLGregorianCalendar(timeString);
} }

View file

@ -1,5 +1,7 @@
embed-server --server-config=${server.config:standalone.xml} embed-server --server-config=${server.config:standalone.xml}
/subsystem=logging/logger=org.keycloak.adapters:add(level=DEBUG) /subsystem=logging/logger=org.keycloak.adapters:add(level=DEBUG)
/subsystem=logging/logger=org.keycloak.saml:add(level=DEBUG)
/subsystem=logging/logger=org.keycloak.testsuite.adapter:add(level=DEBUG)
/subsystem=logging/logger=org.keycloak.subsystem.adapter:add(level=DEBUG) /subsystem=logging/logger=org.keycloak.subsystem.adapter:add(level=DEBUG)
/subsystem=logging/console-handler=CONSOLE:change-log-level(level=DEBUG) /subsystem=logging/console-handler=CONSOLE:change-log-level(level=DEBUG)

View file

@ -24,7 +24,7 @@ import org.keycloak.adapters.saml.SamlPrincipal;
import org.keycloak.adapters.spi.AuthenticationError; import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants; import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import javax.servlet.ServletException; import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
@ -109,7 +109,7 @@ public class SendUsernameServlet {
@Path("error.html") @Path("error.html")
public Response errorPagePost() { public Response errorPagePost() {
authError = (SamlAuthenticationError) httpServletRequest.getAttribute(AuthenticationError.class.getName()); authError = (SamlAuthenticationError) httpServletRequest.getAttribute(AuthenticationError.class.getName());
Integer statusCode = (Integer) httpServletRequest.getAttribute("javax.servlet.error.status_code"); Integer statusCode = (Integer) httpServletRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
System.out.println("In SendUsername Servlet errorPage() status code: " + statusCode); System.out.println("In SendUsername Servlet errorPage() status code: " + statusCode);
return Response.ok(getErrorOutput(statusCode)).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_TYPE + ";charset=UTF-8").build(); return Response.ok(getErrorOutput(statusCode)).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_TYPE + ";charset=UTF-8").build();

View file

@ -0,0 +1,36 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.adapter.page;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import java.net.URL;
public class SalesPostClockSkewServlet extends SAMLServlet {
public static final String DEPLOYMENT_NAME = "sales-post-clock-skew";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;
@Override
public URL getInjectedUrl() {
return url;
}
}

View file

@ -46,7 +46,8 @@ public class AdapterTestExecutionDecider implements TestExecutionDecider {
} }
if (testContext.isAdapterContainerEnabled() || testContext.isAdapterContainerEnabledCluster()) { if (testContext.isAdapterContainerEnabled() || testContext.isAdapterContainerEnabledCluster()) {
if (method.isAnnotationPresent(AppServerContainer.class)) { // taking method level annotation first as it has higher priority // taking method level annotation first as it has higher priority
if (method.isAnnotationPresent(AppServerContainers.class) || method.isAnnotationPresent(AppServerContainer.class)) {
if (getCorrespondingAnnotation(method) == null) { //no corresponding annotation - taking class level annotation if (getCorrespondingAnnotation(method) == null) { //no corresponding annotation - taking class level annotation
if (getCorrespondingAnnotation(testContext.getTestClass()).skip()) { if (getCorrespondingAnnotation(testContext.getTestClass()).skip()) {
return ExecutionDecision.dontExecute("Skipped by @AppServerContainer class level annotation."); return ExecutionDecision.dontExecute("Skipped by @AppServerContainer class level annotation.");
@ -55,7 +56,8 @@ public class AdapterTestExecutionDecider implements TestExecutionDecider {
return ExecutionDecision.dontExecute("Skipped by @AppServerContainer method level annotation."); return ExecutionDecision.dontExecute("Skipped by @AppServerContainer method level annotation.");
} }
} else { //taking class level annotation } else { //taking class level annotation
if (getCorrespondingAnnotation(testContext.getTestClass()).skip()) { if (getCorrespondingAnnotation(testContext.getTestClass()) == null ||
getCorrespondingAnnotation(testContext.getTestClass()).skip()) {
return ExecutionDecision.dontExecute("Skipped by @AppServerContainer class level annotation."); return ExecutionDecision.dontExecute("Skipped by @AppServerContainer class level annotation.");
} }
} }

View file

@ -38,13 +38,15 @@ import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
import org.wildfly.extras.creaper.core.online.OnlineOptions; import org.wildfly.extras.creaper.core.online.OnlineOptions;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot; import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot;
@ -63,17 +65,32 @@ public class AppServerTestEnricher {
@Inject private Instance<TestContext> testContextInstance; @Inject private Instance<TestContext> testContextInstance;
private TestContext testContext; private TestContext testContext;
public static List<String> getAppServerQualifiers(Class testClass) { public static Set<String> getAppServerQualifiers(Class testClass) {
Set<String> appServerQualifiers = new HashSet<>();
Class<?> annotatedClass = getNearestSuperclassWithAppServerAnnotation(testClass); Class<?> annotatedClass = getNearestSuperclassWithAppServerAnnotation(testClass);
if (annotatedClass == null) return null; // no @AppServerContainer annotation --> no adapter test if (annotatedClass != null) {
AppServerContainer[] appServerContainers = annotatedClass.getAnnotationsByType(AppServerContainer.class); AppServerContainer[] appServerContainers = annotatedClass.getAnnotationsByType(AppServerContainer.class);
for (AppServerContainer appServerContainer : appServerContainers) {
appServerQualifiers.add(appServerContainer.value());
}
List<String> appServerQualifiers = new ArrayList<>();
for (AppServerContainer appServerContainer : appServerContainers) {
appServerQualifiers.add(appServerContainer.value());
} }
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(AppServerContainers.class)) {
for (AppServerContainer appServerContainer : method.getAnnotation(AppServerContainers.class).value()) {
appServerQualifiers.add(appServerContainer.value());
}
}
if (method.isAnnotationPresent(AppServerContainer.class)) {
appServerQualifiers.add(method.getAnnotation(AppServerContainer.class).value());
}
}
return appServerQualifiers; return appServerQualifiers;
} }
@ -115,8 +132,8 @@ public class AppServerTestEnricher {
public void updateTestContextWithAppServerInfo(@Observes(precedence = 1) BeforeClass event) { public void updateTestContextWithAppServerInfo(@Observes(precedence = 1) BeforeClass event) {
testContext = testContextInstance.get(); testContext = testContextInstance.get();
List<String> appServerQualifiers = getAppServerQualifiers(testContext.getTestClass()); Set<String> appServerQualifiers = getAppServerQualifiers(testContext.getTestClass());
if (appServerQualifiers == null) { // no adapter test if (appServerQualifiers.isEmpty()) { // no adapter test
log.info("\n\n" + testContext); log.info("\n\n" + testContext);
return; return;
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.arquillian;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects; import java.util.Objects;
import java.util.List; import java.util.List;
import java.util.Set;
import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription; import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription;
import org.jboss.arquillian.container.spi.client.deployment.TargetDescription; import org.jboss.arquillian.container.spi.client.deployment.TargetDescription;
import org.jboss.arquillian.core.api.Instance; import org.jboss.arquillian.core.api.Instance;
@ -60,8 +61,8 @@ public class DeploymentTargetModifier extends AnnotationDeploymentScenarioGenera
List<DeploymentDescription> deployments = super.generate(testClass); List<DeploymentDescription> deployments = super.generate(testClass);
checkTestDeployments(deployments, testClass, context.isAdapterTest()); checkTestDeployments(deployments, testClass, context.isAdapterTest());
List<String> appServerQualifiers = getAppServerQualifiers(testClass.getJavaClass()); Set<String> appServerQualifiers = getAppServerQualifiers(testClass.getJavaClass());
if (appServerQualifiers == null) return deployments; // no adapter test if (appServerQualifiers.isEmpty()) return deployments; // no adapter test
String appServerQualifier = appServerQualifiers.stream() String appServerQualifier = appServerQualifiers.stream()
.filter(q -> q.contains(AppServerTestEnricher.CURRENT_APP_SERVER)) .filter(q -> q.contains(AppServerTestEnricher.CURRENT_APP_SERVER))

View file

@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -28,6 +29,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getAppServerQualifiers; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getAppServerQualifiers;
import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.util.TestCleanup; import org.keycloak.testsuite.util.TestCleanup;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
/** /**
* *
@ -91,20 +93,19 @@ public final class TestContext {
} }
public boolean isAdapterTest() { public boolean isAdapterTest() {
return getAppServerQualifiers(testClass) != null; return !getAppServerQualifiers(testClass).isEmpty();
} }
public boolean isAdapterContainerEnabled() { public boolean isAdapterContainerEnabled() {
if (!isAdapterTest()) return false; //no adapter test if (!isAdapterTest()) return false; //no adapter test
if (appServerInfo == null) return false; return getAppServerQualifiers(testClass).contains(ContainerConstants.APP_SERVER_PREFIX + AppServerTestEnricher.CURRENT_APP_SERVER);
return getAppServerQualifiers(testClass).contains(appServerInfo.getQualifier());
} }
public boolean isAdapterContainerEnabledCluster() { public boolean isAdapterContainerEnabledCluster() {
if (!isAdapterTest()) return false; //no adapter test if (!isAdapterTest()) return false; //no adapter test
if (appServerBackendsInfo.isEmpty()) return false; //no adapter clustered test if (appServerBackendsInfo.isEmpty()) return false; //no adapter clustered test
List<String> appServerQualifiers = getAppServerQualifiers(testClass); Set<String> appServerQualifiers = getAppServerQualifiers(testClass);
String qualifier = appServerBackendsInfo.stream() String qualifier = appServerBackendsInfo.stream()
.map(ContainerInfo::getQualifier) .map(ContainerInfo::getQualifier)

View file

@ -23,6 +23,7 @@ import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive; import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter; import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.utils.arquillian.DeploymentArchiveProcessorUtils; import org.keycloak.testsuite.utils.arquillian.DeploymentArchiveProcessorUtils;
import org.keycloak.testsuite.utils.io.IOUtil; import org.keycloak.testsuite.utils.io.IOUtil;
@ -98,6 +99,14 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
} }
public static WebArchive samlServletDeployment(String name, String webXMLPath, Class... servletClasses) { public static WebArchive samlServletDeployment(String name, String webXMLPath, Class... servletClasses) {
return samlServletDeployment(name, webXMLPath, null, servletClasses);
}
public static WebArchive samlServletDeployment(String name, String webXMLPath, Integer clockSkewSec, Class... servletClasses) {
return samlServletDeployment(name, name, webXMLPath, clockSkewSec, servletClasses);
}
public static WebArchive samlServletDeployment(String name, String customArchiveName, String webXMLPath, Integer clockSkewSec, Class... servletClasses) {
String baseSAMLPath = "/adapter-test/keycloak-saml/"; String baseSAMLPath = "/adapter-test/keycloak-saml/";
String webInfPath = baseSAMLPath + name + "/WEB-INF/"; String webInfPath = baseSAMLPath + name + "/WEB-INF/";
@ -107,15 +116,23 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
URL webXML = AbstractServletsAdapterTest.class.getResource(baseSAMLPath + webXMLPath); URL webXML = AbstractServletsAdapterTest.class.getResource(baseSAMLPath + webXMLPath);
Assert.assertNotNull("web.xml should be in " + baseSAMLPath + webXMLPath, keycloakSAMLConfig); Assert.assertNotNull("web.xml should be in " + baseSAMLPath + webXMLPath, keycloakSAMLConfig);
WebArchive deployment = ShrinkWrap.create(WebArchive.class, name + ".war") WebArchive deployment = ShrinkWrap.create(WebArchive.class, customArchiveName + ".war")
.addClasses(servletClasses) .addClasses(servletClasses)
.addAsWebInfResource(keycloakSAMLConfig, "keycloak-saml.xml")
.addAsWebInfResource(jbossDeploymentStructure, JBOSS_DEPLOYMENT_STRUCTURE_XML); .addAsWebInfResource(jbossDeploymentStructure, JBOSS_DEPLOYMENT_STRUCTURE_XML);
String webXMLContent; String webXMLContent;
try { try {
webXMLContent = IOUtils.toString(webXML.openStream(), Charset.forName("UTF-8")) webXMLContent = IOUtils.toString(webXML.openStream(), Charset.forName("UTF-8"))
.replace("%CONTEXT_PATH%", name); .replace("%CONTEXT_PATH%", name);
if (clockSkewSec != null) {
String keycloakSamlXMLContent = IOUtils.toString(keycloakSAMLConfig.openStream(), Charset.forName("UTF-8"))
.replace("%CLOCK_SKEW%", String.valueOf(clockSkewSec));
deployment.addAsWebInfResource(new StringAsset(keycloakSamlXMLContent), "keycloak-saml.xml");
} else {
deployment.addAsWebInfResource(keycloakSAMLConfig, "keycloak-saml.xml");
}
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -193,9 +210,9 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
.queryParam(AdapterActionsFilter.TIME_OFFSET_PARAM, timeOffset) .queryParam(AdapterActionsFilter.TIME_OFFSET_PARAM, timeOffset)
.build().toString(); .build().toString();
driver.navigate().to(timeOffsetUri); DroneUtils.getCurrentDriver().navigate().to(timeOffsetUri);
WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); WaitUtils.waitUntilElement(By.tagName("body")).is().visible();
String pageSource = driver.getPageSource(); String pageSource = DroneUtils.getCurrentDriver().getPageSource();
System.out.println(pageSource); System.out.println(pageSource);
} }
} }

View file

@ -0,0 +1,167 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.adapter.servlet;
import java.util.List;
import org.apache.http.util.EntityUtils;
import org.hamcrest.Matcher;
import org.jboss.arquillian.container.test.api.Deployer;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.adapters.rotation.PublicKeyLocator;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
import org.keycloak.testsuite.utils.io.IOUtil;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import org.jboss.arquillian.graphene.page.Page;
import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
import org.keycloak.testsuite.adapter.page.SalesPostClockSkewServlet;
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
@AppServerContainer(ContainerConstants.APP_SERVER_EAP71)
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY92)
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY93)
@AppServerContainer(ContainerConstants.APP_SERVER_JETTY94)
public class SAMLClockSkewAdapterTest extends AbstractServletsAdapterTest {
@Page protected SalesPostClockSkewServlet salesPostClockSkewServletPage;
private static final String DEPLOYMENT_NAME_3_SEC = SalesPostClockSkewServlet.DEPLOYMENT_NAME + "_3Sec";
private static final String DEPLOYMENT_NAME_30_SEC = SalesPostClockSkewServlet.DEPLOYMENT_NAME + "_30Sec";
@ArquillianResource private Deployer deployer;
@Deployment(name = DEPLOYMENT_NAME_3_SEC, managed = false)
protected static WebArchive salesPostClockSkewServlet3Sec() {
return samlServletDeployment(SalesPostClockSkewServlet.DEPLOYMENT_NAME, DEPLOYMENT_NAME_3_SEC, SalesPostClockSkewServlet.DEPLOYMENT_NAME + "/WEB-INF/web.xml", 3, AdapterActionsFilter.class, PublicKeyLocator.class, SendUsernameServlet.class);
}
@Deployment(name = DEPLOYMENT_NAME_30_SEC, managed = false)
protected static WebArchive salesPostClockSkewServlet30Sec() {
return samlServletDeployment(SalesPostClockSkewServlet.DEPLOYMENT_NAME, DEPLOYMENT_NAME_30_SEC, SalesPostClockSkewServlet.DEPLOYMENT_NAME + "/WEB-INF/web.xml", 30, AdapterActionsFilter.class, PublicKeyLocator.class, SendUsernameServlet.class);
}
@Deployment(name = SalesPostClockSkewServlet.DEPLOYMENT_NAME, managed = false)
protected static WebArchive salesPostClockSkewServlet5Sec() {
return samlServletDeployment(SalesPostClockSkewServlet.DEPLOYMENT_NAME, SalesPostClockSkewServlet.DEPLOYMENT_NAME + "/WEB-INF/web.xml", 5, AdapterActionsFilter.class, PublicKeyLocator.class, SendUsernameServlet.class);
}
@Override
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
}
private void assertOutcome(int timeOffset, Matcher matcher) throws Exception {
try {
String resultPage = new SamlClientBuilder()
.navigateTo(salesPostClockSkewServletPage.toString())
.processSamlResponse(POST).build()
.login().user(bburkeUser).build()
.processSamlResponse(POST)
.transformDocument(doc -> {
setAdapterAndServerTimeOffset(timeOffset, salesPostClockSkewServletPage.toString() + "unsecured");
return doc;
}).build().executeAndTransform(resp -> EntityUtils.toString(resp.getEntity()));
Assert.assertThat(resultPage, matcher);
} finally {
setAdapterAndServerTimeOffset(0);
}
}
private void assertTokenIsNotValid(int timeOffset) throws Exception {
deployer.deploy(DEPLOYMENT_NAME_3_SEC);
try {
assertOutcome(timeOffset, allOf(
not(containsString("request-path: principal=bburke")),
containsString("SAMLRequest"),
containsString("FORM METHOD=\"POST\"")
));
} finally {
deployer.undeploy(DEPLOYMENT_NAME_3_SEC);
}
}
@Test
public void testTokenHasExpired() throws Exception {
assertTokenIsNotValid(65);
}
@Test
public void testTokenIsNotYetValid() throws Exception {
assertTokenIsNotValid(-65);
}
@Test
public void testTokenTimeIsValid() throws Exception {
deployer.deploy(DEPLOYMENT_NAME_30_SEC);
try {
assertOutcome(-10, allOf(containsString("request-path:"), containsString("principal=bburke")));
} finally {
deployer.undeploy(DEPLOYMENT_NAME_30_SEC);
}
}
@Test
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT7)
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT8)
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT9)
@AppServerContainer(value = ContainerConstants.APP_SERVER_UNDERTOW, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_WILDFLY, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_EAP, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_EAP6, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_EAP71, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_JETTY92, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_JETTY93, skip = true)
@AppServerContainer(value = ContainerConstants.APP_SERVER_JETTY94, skip = true)
public void testClockSkewTomcat() throws Exception {
/*
* Tomcat by default determines context path from name of hot deployed war,
* because of that we need to have this specific test for tomcat containers
*/
deployer.deploy(SalesPostClockSkewServlet.DEPLOYMENT_NAME);
try {
assertOutcome(-4, allOf(containsString("request-path:"), containsString("principal=bburke")));
assertTokenIsNotValid(65);
assertTokenIsNotValid(-65);
} finally {
deployer.undeploy(SalesPostClockSkewServlet.DEPLOYMENT_NAME);
}
}
}

View file

@ -0,0 +1,45 @@
<!--
~ Copyright 2019 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_11.xsd">
<SP entityID="http://localhost:8280/sales-post-clock-skew/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false">
<PrincipalNameMapping policy="FROM_NAME_ID"/>
<RoleIdentifiers>
<Attribute name="Role"/>
</RoleIdentifiers>
<IDP entityID="idp">
<SingleSignOnService requestBinding="POST"
bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
/>
<SingleLogoutService
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
redirectBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
/>
<AllowedClockSkew>%CLOCK_SKEW%</AllowedClockSkew>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2019 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>%CONTEXT_PATH%</module-name>
<servlet>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<error-page>
<location>/error.html</location>
</error-page>
<filter>
<filter-name>AdapterActionsFilter</filter-name>
<filter-class>org.keycloak.testsuite.adapter.filter.AdapterActionsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AdapterActionsFilter</filter-name>
<url-pattern>/unsecured/*</url-pattern>
</filter-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>Application</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>manager</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Unsecured</web-resource-name>
<url-pattern>/unsecured/*</url-pattern>
</web-resource-collection>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK-SAML</auth-method>
<realm-name>demo</realm-name>
</login-config>
<security-role>
<role-name>manager</role-name>
</security-role>
</web-app>

View file

@ -227,6 +227,20 @@
"saml_idp_initiated_sso_url_name": "sales-post" "saml_idp_initiated_sso_url_name": "sales-post"
} }
}, },
{
"clientId": "http://localhost:8280/sales-post-clock-skew/",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8080/sales-post-clock-skew",
"redirectUris": [
"http://localhost:8080/sales-post-clock-skew/*"
],
"attributes": {
"saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name": "sales-post-clock-skew"
}
},
{ {
"clientId": "http://localhost:8280/sales-post-passive/", "clientId": "http://localhost:8280/sales-post-passive/",
"enabled": true, "enabled": true,

View file

@ -31,6 +31,7 @@ import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePath; import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.Node; import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.asset.ClassAsset; import org.jboss.shrinkwrap.api.asset.ClassAsset;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive; import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
@ -61,13 +62,7 @@ public class UndertowDeployerHelper {
public DeploymentInfo getDeploymentInfo(UndertowContainerConfiguration config, WebArchive archive, DeploymentInfo di) { public DeploymentInfo getDeploymentInfo(UndertowContainerConfiguration config, WebArchive archive, DeploymentInfo di) {
String archiveName = archive.getName(); String archiveName = archive.getName();
String contextPath = getContextPath(archive);
String appName = archive.getName().substring(0, archive.getName().lastIndexOf('.'));
if (appName.contains(System.getProperty("project.version"))) {
appName = archive.getName().substring(0, archive.getName().lastIndexOf("-" + System.getProperty("project.version")));
}
String contextPath = "/" + appName;
String appContextUrl = "http://" + config.getBindAddress() + ":" + config.getBindHttpPort() + contextPath; String appContextUrl = "http://" + config.getBindAddress() + ":" + config.getBindHttpPort() + contextPath;
try { try {
@ -208,4 +203,12 @@ public class UndertowDeployerHelper {
} }
private String getContextPath(WebArchive archive) {
if (archive.contains("/META-INF/context.xml") && (archive.get("/META-INF/context.xml").getAsset() instanceof StringAsset)) {
StringAsset asset = (StringAsset) archive.get("/META-INF/context.xml").getAsset();
return asset.getSource().split("path=\"")[1].split("\"")[0];
} else {
return "/".concat(archive.getName().replace(".war", ""));
}
}
} }