[keycloak-dev] Support for more flexible SAML handling for Attribute, AttributeValue and AttributeStatement elements

Thomas Darimont thomas.darimont at googlemail.com
Thu Feb 28 14:01:41 EST 2019


Hello,

In my current project I needed to support extensive customizations of SAML
"Attribute"-elements in Keycloak SAML responses.
Unfortunately keycloaks
"org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper" was not
powerful enough to support
all required customizations.

Some features that were missing / problems I faced were:
- Support for custom AttributeValue types, c.f. "xsd:anyURI" to represent
urn:oid values
- Support for complex nested elements (see example below)
- SAMLAttributeStatementMapper's effectivenes is restricted by
org.keycloak.saml.processing.core.saml.v2.writers.BaseWriter.writeAttributeTypeWithoutRootTag
- Lacking support for custom core protocol implementations, like "saml"
- provider-id "saml" should not be used to identify the "protocol"

Here is an example "AttributeStatement" fragment that I needed to support.
...
        <AttributeStatement>
            <Attribute Name="Role"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
                <AttributeValue xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance"
xsi:type="xsd:string">dummy</AttributeValue>
            </Attribute>

            <Attribute FriendlyName="XSPA Organization ID"
                Name="urn:oasis:names:tc:xspa:1.0:subject:organization-id"

NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
                <AttributeValue xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance"
xsi:type="xsd:anyURI">urn:oid:1.2.3.4.5.6.7.8.9.10.11.12</AttributeValue>
            </Attribute>

            <Attribute FriendlyName="Acme Role"
Name="urn:oasis:names:tc:xacml:2.0:subject:role"

NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
                <AttributeValue xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:anyType">
                <Role code="PRA" codeSystem="1.2.3.4.5.6.7.8.9.10.11.12"
                          codeSystemName="IHEXDShealthcareFacilityTypeCode"
displayName="Doctor's office" xmlns="urn:hl7-org:v3"/>
                </AttributeValue>
            </Attribute>
        </AttributeStatement>
...
a full example for a complete SAML response can be found here for
reference:
https://gist.github.com/thomasdarimont/02a7861562d0684861b213a1d16b7047

I eventually managed to customize the SAML responses as needed, but my
approach is probably not 100% future proof...
If anyone here knows a better way to do this, I'm all ears :)

Nevertheless, it would be great if Keycloaks support for custom SAML
attributes would be more flexible in the future.

Anyways, here is what I had to do in order to get the required SAML
customizations working:

- Create a "org.keycloak.protocol.LoginProtocolFactory" extension and
reference "CustomSamlProtocolFactory"
- Create class CustomSamlProtocolFactory extends SamlProtocolFactory
- Create class CustomSamlProtocol extends SamlProtocol and return a new
CustomSamlProtocol instance in CustomSamlProtocolFactory#create(..) (see
below)
- override
org.keycloak.protocol.saml.SamlProtocol#buildAuthenticatedResponse
  -> protected Response
buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession,
String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder
bindingBuilder)
  -> samlDocument contains the SAML document populated by Keycloak BEFORE
it is signed and encrypted
  -> clientSession gives you access to the current user etc.
  -> add custom SAML processing as needed
  -> call return super.buildAuthenticatedResponse(clientSession,
redirectUri, samlDocument, bindingBuilder); at the end, this potentially
signs and encrypts the SAML document
- Make keycloak aware of that extension... (tricky, read on)

Note that the "CustomSamlProtocolFactory" uses the same provider ID "saml"
as the "SamlProtocolFactory" does -> this leads to a name clash...
unfortunantely Keycloak currently doesn't support to have custom
implementations for core protocols.

Here comes the hack:
In order to let Keycloak pickup my "CustomSamlProtocolFactory"
implementation before it's own "SamlProtocolFactory", I needed to copy the
provider jar to
`$KEYCLOAK_HOME/modules/system/layers/keycloak/org/keycloak/keycloak-services/main`
and change the keycloak-services module.xml to:

    <resources>
        <resource-root
path="simple-custom-saml-protocol-1.0.0.0-SNAPSHOT.jar"/> <!-- added this
-->
        <resource-root path="keycloak-services-4.8.3.Final.jar"/>
    </resources>

With that change Keycloak picked up my custom "CustomSamlProtocolFactory"
first and I could adjust the SAML document as required.

Btw. you might be wondering why I didn't just create a new
"CustomSamlProtocolFactory" with a dedicated provider-id like "saml-custom"
and use that for clients. We'll that was my first try, but unfortunately
this is currently not possible, since the Admin-Console
UI for the SAML configuration seems to be hard-coded against the
provider-id "saml"... so I'm stuck with the current "solution".
It would be great if the "LoginProtocolFactory" SPI would support custom
protocol implementations, while reusing protocol configuration options and
the admin-console ui.

Cheers,
Thomas

Ps.:

FYI this is what a SAMLAttributeStatementMapper allows you to do currently
(Keycloak 4.8.3.Final):

// SimpleSamlMapper
...
    @Override
    public void transformAttributeStatement(AttributeStatementType
attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession
session, UserSessionModel userSession, AuthenticatedClientSessionModel
clientSession) {

        // transform attributeStatement here
        LOGGER.infof("transformAttributeStatement");

        AttributeType bubu = new AttributeType("bubu");
        bubu.setFriendlyName("FriendlyBubu");

bubu.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:basic");
        bubu.setName("Bubu");

        bubu.addAttributeValue("Object allowed but only Strings or
NameIDType supported here...");
        // see: bottom of
org.keycloak.saml.processing.core.saml.v2.writers.BaseWriter.writeAttributeTypeWithoutRootTag

        // would be great to have support for AttributeValue type
customizations, even if value is String.
        // would be great to have support for complex XML element as values

        attributeStatement.addAttribute(new
AttributeStatementType.ASTChoiceType(bubu));
    }
...

// CustomSamlProtocolFactory
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.saml.SamlProtocolFactory;

public class CustomSamlProtocolFactory extends SamlProtocolFactory {

    public LoginProtocol create(KeycloakSession session) {
        return (new CustomSamlProtocol()).setSession(session);
    }
}


// CustomSamlProtocol
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.ws.rs.core.Response;
import java.io.IOException;

public class CustomSamlProtocol extends SamlProtocol {

    protected Response
buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession,
String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder
bindingBuilder) throws ConfigurationException, ProcessingException,
IOException {

        // TODO use Keycloak provider information from this.session
        Element assertionElement = (Element)
samlDocument.getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
"Assertion").item(0);

        Element attributeStatementElement = (Element)
assertionElement.getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
"AttributeStatement").item(0);
        if (attributeStatementElement == null) {
            attributeStatementElement =
samlDocument.createElementNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
"AttributeStatement");
            assertionElement.appendChild(attributeStatementElement);
        }
        // TODO pull information from user attributes

attributeStatementElement.appendChild(newSamlAttributeElement(samlDocument,
null, "Role", JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get(), "dummy",
"xsd:string"));

        // see: http://oid-info.com/get/

attributeStatementElement.appendChild(newSamlAttributeElement(samlDocument,
"XSPA Organization ID",
"urn:oasis:names:tc:xspa:1.0:subject:organization-id",
JBossSAMLURIConstants.ATTRIBUTE_FORMAT_URI.get(),
"urn:oid:1.2.3.4.5.6.7.8.9.10.11.12", "xsd:anyURI"));

        Element roleElement =
samlDocument.createElementNS("urn:hl7-org:v3", "Role");
        roleElement.setAttribute("code", "PRA");
        roleElement.setAttribute("codeSystem",
"1.2.3.4.5.6.7.8.9.10.11.12");
        roleElement.setAttribute("codeSystemName",
"IHEXDShealthcareFacilityTypeCode");
        roleElement.setAttribute("displayName", "Doctor's office");

attributeStatementElement.appendChild(newSamlAttributeElement(samlDocument,
"Acme Role", "urn:oasis:names:tc:xacml:2.0:subject:role",
JBossSAMLURIConstants.ATTRIBUTE_FORMAT_URI.get(), roleElement,
"xsd:anyType"));

        return super.buildAuthenticatedResponse(clientSession, redirectUri,
samlDocument, bindingBuilder);
    }

    private Element newSamlAttributeElement(Document samlDocument, String
friendlyName, String name, String nameFormat, Object value, String type) {

        Element targetSamlAttributeElement =
samlDocument.createElementNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
"Attribute");

        if (friendlyName != null) {
            targetSamlAttributeElement.setAttribute("FriendlyName",
friendlyName);
        }
        targetSamlAttributeElement.setAttribute("Name", name);
        if (nameFormat != null) {
            targetSamlAttributeElement.setAttribute("NameFormat",
nameFormat);
        }

        Element samlAttributeValue =
samlDocument.createElementNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
"AttributeValue");
        samlAttributeValue.setAttribute("xmlns:xsi", "
http://www.w3.org/2001/XMLSchema-instance");
        samlAttributeValue.setAttribute("xsi:type", type);
        targetSamlAttributeElement.appendChild(samlAttributeValue);

        if (value instanceof String) {
            samlAttributeValue.setTextContent((String) value);
        } else if (value instanceof Element) {
            samlAttributeValue.appendChild((Element) value);
        } else if (value != null) {
            samlAttributeValue.setTextContent(value.toString());
        } else {
            samlAttributeValue.setTextContent(String.valueOf(value));
        }

        return targetSamlAttributeElement;
    }
}


More information about the keycloak-dev mailing list