KEYCLOAK-10757: Replaying assertion with signature in SAML adapters

This commit is contained in:
rmartinc 2019-07-03 12:21:32 +02:00 committed by Hynek Mlnařík
parent 1887d3b038
commit 7f54a57271
30 changed files with 430 additions and 84 deletions

View file

@ -29,6 +29,7 @@ public class Constants {
static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
static final String LOGOUT_PAGE = "logoutPage";
static final String FORCE_AUTHENTICATION = "forceAuthentication";
static final String KEEP_DOM_ASSERTION = "keepDOMAssertion";
static final String IS_PASSIVE = "isPassive";
static final String TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN = "turnOffChangeSessionIdOnLogin";
static final String ROLE_ATTRIBUTES = "RoleIdentifiers";
@ -83,6 +84,7 @@ public class Constants {
static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
static final String LOGOUT_PAGE = "logoutPage";
static final String FORCE_AUTHENTICATION = "forceAuthentication";
static final String KEEP_DOM_ASSERTION = "keepDOMAssertion";
static final String ROLE_IDENTIFIERS = "RoleIdentifiers";
static final String SIGNING = "signing";
static final String ENCRYPTION = "encryption";

View file

@ -60,6 +60,11 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.setXmlName(Constants.XML.FORCE_AUTHENTICATION)
.build();
static final SimpleAttributeDefinition KEEP_DOM_ASSERTION =
new SimpleAttributeDefinitionBuilder(Constants.Model.KEEP_DOM_ASSERTION, ModelType.BOOLEAN, true)
.setXmlName(Constants.XML.KEEP_DOM_ASSERTION)
.build();
static final SimpleAttributeDefinition IS_PASSIVE =
new SimpleAttributeDefinitionBuilder(Constants.Model.IS_PASSIVE, ModelType.BOOLEAN, true)
.setXmlName(Constants.XML.IS_PASSIVE)
@ -96,7 +101,7 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.build();
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION,
IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN};
IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN, KEEP_DOM_ASSERTION};
static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES,
ROLE_MAPPINGS_PROVIDER_ID, ROLE_MAPPINGS_PROVIDER_CONFIG};

View file

@ -32,6 +32,7 @@ keycloak-saml.SP.sslPolicy=SSL Policy to use
keycloak-saml.SP.nameIDPolicyFormat=Name ID policy format URN
keycloak-saml.SP.logoutPage=URI to a logout page
keycloak-saml.SP.forceAuthentication=Redirected unauthenticated request to a login page
keycloak-saml.SP.keepDOMAssertion=Attribute to inject the DOM representation of the assertion into the SamlPrincipal (respecting the original syntax)
keycloak-saml.SP.isPassive=If user isn't logged in just return with an error. Used to check if a user is already logged in or not
keycloak-saml.SP.turnOffChangeSessionIdOnLogin=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak-saml.SP.RoleIdentifiers=Role identifiers

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ 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");
@ -84,6 +84,11 @@
<xs:documentation>Redirected unauthenticated request to a login page</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="keepDOMAssertion" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Attribute to inject the DOM representation of the assertion into the SamlPrincipal (respecting the original syntax). Default value is false</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>If user isn't logged in just return with an error. Used to check if a user is already logged in or not</xs:documentation>

View file

@ -15,12 +15,13 @@
~ limitations under the License.
-->
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.2">
<secure-deployment name="my-app.war">
<SP entityID="http://localhost:8080/sales-post-enc/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
keepDOMAssertion="false"
forceAuthentication="false">
<Keys>

View file

@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.w3c.dom.Document;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -43,14 +44,20 @@ public class SamlPrincipal implements Serializable, Principal {
private String samlSubject;
private String nameIDFormat;
private AssertionType assertion;
private Document assertionDocument;
public SamlPrincipal(AssertionType assertion, String name, String samlSubject, String nameIDFormat, MultivaluedHashMap<String, String> attributes, MultivaluedHashMap<String, String> friendlyAttributes) {
this(assertion, null, name, samlSubject, nameIDFormat, attributes, friendlyAttributes);
}
public SamlPrincipal(AssertionType assertion, Document assertionDocument, String name, String samlSubject, String nameIDFormat, MultivaluedHashMap<String, String> attributes, MultivaluedHashMap<String, String> friendlyAttributes) {
this.name = name;
this.attributes = attributes;
this.friendlyAttributes = friendlyAttributes;
this.samlSubject = samlSubject;
this.nameIDFormat = nameIDFormat;
this.assertion = assertion;
this.assertionDocument = assertionDocument;
}
public SamlPrincipal() {
@ -104,6 +111,16 @@ public class SamlPrincipal implements Serializable, Principal {
return res;
}
/*
* The assertion element in DOM format, to respect the original syntax.
* It's only available if option <em>keepDOMAssertion</em> is set to true.
*
* @return The document assertion or null
*/
public Document getAssertionDocument() {
return assertionDocument;
}
@Override
public String getName() {
return name;

View file

@ -315,6 +315,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
private SignatureAlgorithm signatureAlgorithm;
private String signatureCanonicalizationMethod;
private boolean autodetectBearerOnly;
private boolean keepDOMAssertion;
@Override
public boolean turnOffChangeSessionIdOnLogin() {
@ -478,4 +479,13 @@ public class DefaultSamlDeployment implements SamlDeployment {
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
this.autodetectBearerOnly = autodetectBearerOnly;
}
@Override
public boolean isKeepDOMAssertion() {
return keepDOMAssertion;
}
public void setKeepDOMAssertion(Boolean keepDOMAssertion) {
this.keepDOMAssertion = keepDOMAssertion != null && keepDOMAssertion;
}
}

View file

@ -183,4 +183,6 @@ public interface SamlDeployment {
String getPrincipalAttributeName();
boolean isAutodetectBearerOnly();
boolean isKeepDOMAssertion();
}

View file

@ -90,6 +90,7 @@ public class SP implements Serializable {
private RoleMappingsProviderConfig roleMappingsProviderConfig;
private IDP idp;
private boolean autodetectBearerOnly;
private boolean keepDOMAssertion;
public String getEntityID() {
return entityID;
@ -131,6 +132,14 @@ public class SP implements Serializable {
this.turnOffChangeSessionIdOnLogin = turnOffChangeSessionIdOnLogin != null && turnOffChangeSessionIdOnLogin;
}
public boolean isKeepDOMAssertion() {
return keepDOMAssertion;
}
public void setKeepDOMAssertion(Boolean keepDOMAssertion) {
this.keepDOMAssertion = keepDOMAssertion != null && keepDOMAssertion;
}
public List<Key> getKeys() {
return keys;
}

View file

@ -80,6 +80,7 @@ public class DeploymentBuilder {
IDP idp = sp.getIdp();
deployment.setSignatureCanonicalizationMethod(idp.getSignatureCanonicalizationMethod());
deployment.setAutodetectBearerOnly(sp.isAutodetectBearerOnly());
deployment.setKeepDOMAssertion(sp.isKeepDOMAssertion());
deployment.setSignatureAlgorithm(SignatureAlgorithm.RSA_SHA256);
if (idp.getSignatureAlgorithm() != null) {
deployment.setSignatureAlgorithm(SignatureAlgorithm.valueOf(idp.getSignatureAlgorithm()));

View file

@ -90,6 +90,7 @@ public enum KeycloakSamlAdapterV1QNames implements HasQName {
ATTR_VALIDATE_REQUEST_SIGNATURE(null, "validateRequestSignature"),
ATTR_VALIDATE_RESPONSE_SIGNATURE(null, "validateResponseSignature"),
ATTR_VALUE(null, "value"),
ATTR_KEEP_DOM_ASSERTION(null, "keepDOMAssertion"),
UNKNOWN_ELEMENT("")
;

View file

@ -53,6 +53,7 @@ public class SpParser extends AbstractKeycloakSamlAdapterV1Parser<SP> {
sp.setIsPassive(StaxParserUtil.getBooleanAttributeValueRP(element, KeycloakSamlAdapterV1QNames.ATTR_IS_PASSIVE));
sp.setAutodetectBearerOnly(StaxParserUtil.getBooleanAttributeValueRP(element, KeycloakSamlAdapterV1QNames.ATTR_AUTODETECT_BEARER_ONLY));
sp.setTurnOffChangeSessionIdOnLogin(StaxParserUtil.getBooleanAttributeValueRP(element, KeycloakSamlAdapterV1QNames.ATTR_TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN));
sp.setKeepDOMAssertion(StaxParserUtil.getBooleanAttributeValueRP(element, KeycloakSamlAdapterV1QNames.ATTR_KEEP_DOM_ASSERTION));
return sp;
}

View file

@ -375,9 +375,11 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
return AuthOutcome.FAILED;
}
Element assertionElement = null;
if (deployment.getIDP().getSingleSignOnService().validateAssertionSignature()) {
try {
if (!AssertionUtil.isSignatureValid(getAssertionFromResponse(responseHolder), deployment.getIDP().getSignatureValidationKeyLocator())) {
assertionElement = getAssertionFromResponse(responseHolder);
if (!AssertionUtil.isSignatureValid(assertionElement, deployment.getIDP().getSignatureValidationKeyLocator())) {
log.error("Failed to verify saml assertion signature");
challenge = new AuthChallenge() {
@ -493,7 +495,13 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
URI nameFormat = subjectNameID == null ? null : subjectNameID.getFormat();
String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
if (deployment.isKeepDOMAssertion() && assertionElement == null) {
// obtain the assertion from the response to add the DOM document to the principal
assertionElement = getAssertionFromResponseNoException(responseHolder);
}
final SamlPrincipal principal = new SamlPrincipal(assertion,
deployment.isKeepDOMAssertion()? getAssertionDocumentFromElement(assertionElement) : null,
principalName, principalName, nameFormatString, attributes, friendlyAttributes);
final String sessionIndex = authn == null ? null : authn.getSessionIndex();
final XMLGregorianCalendar sessionNotOnOrAfter = authn == null ? null : authn.getSessionNotOnOrAfter();
SamlSession account = new SamlSession(principal, roles, sessionIndex, sessionNotOnOrAfter);
@ -534,6 +542,30 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
return DocumentUtil.getElement(responseHolder.getSamlDocument(), new QName(JBossSAMLConstants.ASSERTION.get()));
}
private Element getAssertionFromResponseNoException(final SAMLDocumentHolder responseHolder) {
try {
return getAssertionFromResponse(responseHolder);
} catch (ConfigurationException|ProcessingException e) {
log.warn("Cannot obtain DOM assertion element", e);
return null;
}
}
private Document getAssertionDocumentFromElement(final Element assertionElement) {
if (assertionElement == null) {
return null;
}
try {
Document assertionDoc = DocumentUtil.createDocument();
assertionDoc.adoptNode(assertionElement);
assertionDoc.appendChild(assertionElement);
return assertionDoc;
} catch (ConfigurationException e) {
log.warn("Cannot obtain DOM assertion document", e);
return null;
}
}
private String getAttributeValue(Object attrValue) {
String value = null;
if (attrValue instanceof String) {

View file

@ -97,6 +97,11 @@
<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="keepDOMAssertion" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Attribute to inject the DOM representation of the assertion into the SamlPrincipal (respecting the original syntax). 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>

View file

@ -86,6 +86,17 @@ public class KeycloakSamlAdapterXMLParserTest {
testValidationValid("keycloak-saml-with-role-mappings-provider.xml");
}
@Test
public void testValidationWithKeepDOMAssertion() throws Exception {
testValidationValid("keycloak-saml-keepdomassertion.xml");
// check keep dom assertion is TRUE
KeycloakSamlAdapter config = parseKeycloakSamlAdapterConfig("keycloak-saml-keepdomassertion.xml", KeycloakSamlAdapter.class);
assertNotNull(config);
assertEquals(1, config.getSps().size());
SP sp = config.getSps().get(0);
assertTrue(sp.isKeepDOMAssertion());
}
@Test
public void testValidationKeyInvalid() throws Exception {
InputStream schemaIs = KeycloakSamlAdapterV1Parser.class.getResourceAsStream(CURRENT_XSD_LOCATION);
@ -115,6 +126,7 @@ public class KeycloakSamlAdapterXMLParserTest {
assertTrue(sp.isForceAuthentication());
assertTrue(sp.isIsPassive());
assertFalse(sp.isAutodetectBearerOnly());
assertFalse(sp.isKeepDOMAssertion());
assertEquals(2, sp.getKeys().size());
Key signing = sp.getKeys().get(0);
assertTrue(signing.isSigning());

View file

@ -0,0 +1,77 @@
<!--
~ 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_9.xsd">
<SP entityID="sp"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="format"
forceAuthentication="true"
keepDOMAssertion="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"
>
<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>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -30,6 +30,7 @@ public class Constants {
static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
static final String LOGOUT_PAGE = "logoutPage";
static final String FORCE_AUTHENTICATION = "forceAuthentication";
static final String KEEP_DOM_ASSERTION = "keepDOMAssertion";
static final String IS_PASSIVE = "isPassive";
static final String TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN = "turnOffChangeSessionIdOnLogin";
static final String ROLE_ATTRIBUTES = "RoleIdentifiers";
@ -86,6 +87,7 @@ public class Constants {
static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
static final String LOGOUT_PAGE = "logoutPage";
static final String FORCE_AUTHENTICATION = "forceAuthentication";
static final String KEEP_DOM_ASSERTION = "keepDOMAssertion";
static final String ROLE_IDENTIFIERS = "RoleIdentifiers";
static final String SIGNING = "signing";
static final String ENCRYPTION = "encryption";

View file

@ -62,6 +62,11 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.setXmlName(Constants.XML.FORCE_AUTHENTICATION)
.build();
static final SimpleAttributeDefinition KEEP_DOM_ASSERTION =
new SimpleAttributeDefinitionBuilder(Constants.Model.KEEP_DOM_ASSERTION, ModelType.BOOLEAN, true)
.setXmlName(Constants.XML.KEEP_DOM_ASSERTION)
.build();
static final SimpleAttributeDefinition IS_PASSIVE =
new SimpleAttributeDefinitionBuilder(Constants.Model.IS_PASSIVE, ModelType.BOOLEAN, true)
.setXmlName(Constants.XML.IS_PASSIVE)
@ -97,7 +102,7 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.build();
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION,
IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN};
IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN, KEEP_DOM_ASSERTION};
static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES,
ROLE_MAPPINGS_PROVIDER_ID, ROLE_MAPPINGS_PROVIDER_CONFIG};

View file

@ -32,6 +32,7 @@ keycloak-saml.SP.sslPolicy=SSL Policy to use
keycloak-saml.SP.nameIDPolicyFormat=Name ID policy format URN
keycloak-saml.SP.logoutPage=URI to a logout page
keycloak-saml.SP.forceAuthentication=Redirected unauthenticated request to a login page
keycloak-saml.SP.keepDOMAssertion=Attribute to inject the DOM representation of the assertion into the SamlPrincipal (respecting the original syntax)
keycloak-saml.SP.isPassive=If user isn't logged in just return with an error. Used to check if a user is already logged in or not
keycloak-saml.SP.turnOffChangeSessionIdOnLogin=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak-saml.SP.RoleIdentifiers=Role identifiers

View file

@ -84,6 +84,11 @@
<xs:documentation>Redirected unauthenticated request to a login page</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="keepDOMAssertion" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Attribute to inject the DOM representation of the assertion into the SamlPrincipal (respecting the original syntax). Default value is false</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>If user isn't logged in just return with an error. Used to check if a user is already logged in or not</xs:documentation>

View file

@ -1,71 +0,0 @@
<!--
~ 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.
-->
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
<secure-deployment name="my-app.war">
<SP entityID="http://localhost:8080/sales-post-enc/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false"
isPassive="true"
turnOffChangeSessionIdOnLogin="true">
<Keys>
<Key encryption="true" signing="true">
<PrivateKeyPem>my_key.pem</PrivateKeyPem>
<PublicKeyPem>my_key.pub</PublicKeyPem>
<CertificatePem>cert.cer</CertificatePem>
<KeyStore resource="/WEB-INF/keystore.jks" password="store123" file="test" alias="test" type="jks">
<PrivateKey alias="http://localhost:8080/sales-post-enc/" password="test123"/>
<Certificate alias="http://localhost:8080/sales-post-enc/"/>
</KeyStore>
</Key>
</Keys>
<PrincipalNameMapping policy="FROM_NAME_ID" attribute="test"/>
<RoleIdentifiers>
<Attribute name="Role"/>
<Attribute name="Role2"/>
</RoleIdentifiers>
<IDP entityID="idp" signaturesRequired="true" signatureAlgorithm="test" signatureCanonicalizationMethod="test">
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
validateAssertionSignature="true"
requestBinding="POST"
responseBinding="POST"
bindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
assertionConsumerServiceUrl="acsUrl"/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="true"
signResponse="true"
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
redirectBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
<Keys>
<Key signing="true">
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
<Certificate alias="saml-demo"/>
</KeyStore>
</Key>
</Keys>
</IDP>
</SP>
</secure-deployment>
</subsystem>

View file

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.2">
<secure-deployment name="my-app.war">
<SP entityID="http://localhost:8080/sales-post-enc/"
sslPolicy="EXTERNAL"

View file

@ -21,6 +21,7 @@
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
keepDOMAssertion="false"
forceAuthentication="false"
isPassive="true"
turnOffChangeSessionIdOnLogin="true">

View file

@ -58,7 +58,7 @@ public class KeycloakSamlSubsystemInstallation implements ClientInstallationProv
@Override
public String getHelpText() {
return "Keycloak SAML adapter Wildfly/JBoss subsystem xml. Put this <subsystem xmlns=\"urn:jboss:domain:keycloak-saml:1.1\"> element of your standalone.xml file.";
return "Keycloak SAML adapter Wildfly/JBoss subsystem xml. Put this <subsystem xmlns=\"urn:jboss:domain:keycloak-saml:1.2\"> element of your standalone.xml file.";
}
@Override

View file

@ -39,10 +39,19 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.datatype.XMLGregorianCalendar;
import java.io.IOException;
import java.io.StringWriter;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -94,6 +103,25 @@ public class SendUsernameServlet {
}
@GET
@Path("getAssertionFromDocument")
public Response getAssertionFromDocument() throws IOException, TransformerException {
sentPrincipal = httpServletRequest.getUserPrincipal();
DocumentBuilderFactory domFact = DocumentBuilderFactory.newInstance();
Document doc = ((SamlPrincipal) sentPrincipal).getAssertionDocument();
String xml = "";
if (doc != null) {
DOMSource domSource = new DOMSource(doc);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(domSource, result);
xml = writer.toString();
}
return Response.ok(xml).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_TYPE + ";charset=UTF-8").build();
}
@GET
@Path("{path}")
public Response doGetElseWhere(@PathParam("path") String path, @QueryParam("checkRoles") boolean checkRolesFlag) throws IOException {

View file

@ -0,0 +1,39 @@
/*
* 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;
/**
* @author rmartinc
*/
public class EmployeeDomServlet extends SAMLServlet {
public static final String DEPLOYMENT_NAME = "employee-dom";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;
@Override
public URL getInjectedUrl() {
return url;
}
}

View file

@ -39,10 +39,13 @@ import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@ -132,6 +135,7 @@ import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.adapter.page.*;
import org.keycloak.testsuite.admin.ApiUtil;
@ -189,6 +193,9 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest {
@Page
protected Employee2Servlet employee2ServletPage;
@Page
protected EmployeeDomServlet employeeDomServletPage;
@Page
protected EmployeeSigServlet employeeSigServletPage;
@ -307,6 +314,11 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest {
return samlServletDeployment(Employee2Servlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
}
@Deployment(name = EmployeeDomServlet.DEPLOYMENT_NAME)
protected static WebArchive employeedom() {
return samlServletDeployment(EmployeeDomServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
}
@Deployment(name = EmployeeSigServlet.DEPLOYMENT_NAME)
protected static WebArchive employeeSig() {
return samlServletDeployment(EmployeeSigServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
@ -1421,6 +1433,10 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest {
waitUntilElement(By.xpath("//body")).text().contains("phone: 617");
waitUntilElement(By.xpath("//body")).text().contains("friendlyAttribute phone: null");
driver.navigate().to(employee2ServletPage.getUriBuilder().clone().path("getAssertionFromDocument").build().toURL());
waitForPageToLoad();
Assert.assertEquals("", driver.getPageSource());
employee2ServletPage.logout();
checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage);
@ -1483,6 +1499,25 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest {
validateXMLWithSchema(driver.getPageSource(), "/adapter-test/keycloak-saml/metadata-schema/saml-schema-metadata-2.0.xsd");
}
@Test
public void testDOMAssertion() throws Exception {
assertSuccessfulLogin(employeeDomServletPage, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke");
assertSuccessfullyLoggedIn(employeeDomServletPage, "principal=bburke");
driver.navigate().to(employeeDomServletPage.getUriBuilder().clone().path("getAssertionFromDocument").build().toURL());
waitForPageToLoad();
String xml = driver.getPageSource();
Assert.assertNotEquals("", xml);
Document doc = DocumentUtil.getDocument(new StringReader(xml));
String certBase64 = DocumentUtil.getElement(doc, new QName("http://www.w3.org/2000/09/xmldsig#", "X509Certificate")).getTextContent();
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate cert = cf.generateCertificate(new ByteArrayInputStream(Base64.decode(certBase64)));
PublicKey pubkey = cert.getPublicKey();
Assert.assertTrue(AssertionUtil.isSignatureValid(doc.getDocumentElement(), pubkey));
employeeDomServletPage.logout();
checkLoggedOut(employeeDomServletPage, testRealmSAMLPostLoginPage);
}
@Test
public void spMetadataValidation() throws Exception {

View file

@ -0,0 +1,65 @@
<!--
~ 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_7.xsd">
<SP entityID="http://localhost:8280/employee-dom/"
sslPolicy="EXTERNAL"
logoutPage="/logout.jsp"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
keepDOMAssertion="true"
forceAuthentication="false">
<Keys>
<Key signing="true" >
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
<PrivateKey alias="http://localhost:8080/employee-dom/" password="store123"/>
<Certificate alias="http://localhost:8080/employee-dom/"/>
</KeyStore>
</Key>
</Keys>
<PrincipalNameMapping policy="FROM_NAME_ID"/>
<RoleIdentifiers>
<Attribute name="Role"/>
</RoleIdentifiers>
<IDP entityID="idp">
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
requestBinding="POST"
bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="true"
signResponse="true"
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
redirectBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
/>
<Keys>
<Key signing="true">
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
<Certificate alias="demo"/>
</KeyStore>
</Key>
</Keys>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -625,6 +625,61 @@
}
]
},
{
"clientId": "http://localhost:8280/employee-dom/",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8080/employee-dom",
"redirectUris": [
"http://localhost:8080/employee-dom/*"
],
"adminUrl": "http://localhost:8080/employee-dom",
"attributes": {
"saml.assertion.signature": "true",
"saml.server.signature": "true",
"saml.client.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.authnstatement": "true",
"saml.signing.certificate": "MIIC+zCCAeOgAwIBAgIEcFrChjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNodHRwOi8vbG9jYWxob3N0OjgwODAvZW1wbG95ZWUtZG9tLzAeFw0xOTA3MDMwOTE1NDlaFw00NjExMTgwOTE1NDlaMC4xLDAqBgNVBAMTI2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9lbXBsb3llZS1kb20vMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmEbjaKmKCh2MXTVLMUXdbjKSdmXAOenuE2bDD0AlEaJmnJ5zU2JY6UuFflH3332n2YktaaCyTznwX1Zcf7GH3bm7xhV1HSmlbFpIY17M8QUOIGZEzvKSbT9gjRJSPIdE1JvZuqgzuXpRlRfC4eoH1VgS0Vmu4gwIRFnUUgqc5hW11AQVkGZs7TkEYbVEYneKMbQOKa1OzW+FAb7C13Yn19gSvGr3THE+7FGwxEJM6N6kr4xnxg4VpaXcsW4ijGI3CHPJA06MZ6LzXxCmz+8TOSLo5pV7GKgME9QR1lBSC2Cp0yDtHjqK6QCqApyHhP2xN8qzJhMIhffSSHq4GokhjwIDAQABoyEwHzAdBgNVHQ4EFgQUOVG/h7cr+T6LJ4dQIVALBknwF/AwDQYJKoZIhvcNAQELBQADggEBAI5Y1MPMHPsDRJBQke/+tkRO4PALbsAQtfvYDNmpBGzUNo2xU3n7PNzbWrcqubjLN0nqXloBTaeeHtrFGAejMCS5X8UOGLyXbKBm7hHJs5ZZASrm0FkUzyuJexWCbSAg0p7Z6wWw03dnV/A9LDFwTdGIYsnSzZ59/v3BUH89mavOwVuVJB5O2PysUob3urcv1tmv9eL5jAMc764ID1gLkydcNrmICa+aZ/FojfReyTtwWX0DoPflPvF/Xllp3jLg1HwSlD6fD2wO/MKawgBbE6xrAkg5bF01B25RadJJffx3hEtgxBzlo1EL4Ir+lJmM1vzuTq4c1wDYKku4Y0Qg5o0="
},
"protocolMappers": [
{
"name": "email",
"protocol": "saml",
"protocolMapper": "saml-user-property-mapper",
"consentRequired": false,
"config": {
"user.attribute": "email",
"friendly.name": "email",
"attribute.name": "urn:oid:1.2.840.113549.1.9.1",
"attribute.nameformat": "URI Reference"
}
},
{
"name": "phone",
"protocol": "saml",
"protocolMapper": "saml-user-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "phone",
"attribute.name": "phone",
"attribute.nameformat": "Basic"
}
},
{
"name": "role-list",
"protocol": "saml",
"protocolMapper": "saml-role-list-mapper",
"consentRequired": false,
"config": {
"attribute.name": "Role",
"attribute.nameformat": "Basic",
"single": "false"
}
}
]
},
{
"clientId": "http://localhost:8280/employee-sig-front/",
"enabled": true,