Skip to content

Commit

Permalink
Add support for multiple Assertion Consumer Services (ACS)
Browse files Browse the repository at this point in the history
This change is 100% backward compatible with the old way of specifying
just one ACS, both from a configuration and from an API point of view.
The generated metadata and AuthnRequest XMLs are also exactly the same
as before when just one ACS is specified with non-indexed properties.

Fixes #328.
  • Loading branch information
mauromol committed Nov 9, 2021
1 parent 043ca5e commit 21e7b45
Show file tree
Hide file tree
Showing 16 changed files with 1,583 additions and 54 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,18 @@ onelogin.saml2.sp.assertion_consumer_service.url = http://localhost:8080/java-sa
# HTTP-POST binding only
onelogin.saml2.sp.assertion_consumer_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST

# The above two settings declare just one Assertion Consumer Service (ACS). As an alternative, it's also
# possible to specify multiple Assertion Consumer Services by providing indexed properties: the index
# is used as the ACS index as well and one of the defined services may be marked as the default one.
# Please note that, when indexed ACS properties are present, the non-indexed ones are ignored.
# Here is a complete example, but remember that Onelogin Toolkit still actually supports HTTP-POST binding
# only for response processing:
#onelogin.saml2.sp.assertion_consumer_service[0].url = http://localhost:8081/java-saml-jspsample/acs1.jsp
#onelogin.saml2.sp.assertion_consumer_service[0].binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
#onelogin.saml2.sp.assertion_consumer_service[1].url = http://localhost:8081/java-saml-jspsample/acs2.jsp
#onelogin.saml2.sp.assertion_consumer_service[1].binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
#onelogin.saml2.sp.assertion_consumer_service[1].default = true

# Specifies info about where and how the <Logout Response> message MUST be
# returned to the requester, in this case our SP.
onelogin.saml2.sp.single_logout_service.url = http://localhost:8080/java-saml-tookit-jspsample/sls.jsp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package com.onelogin.saml2.authn;

import java.net.URL;

import com.onelogin.saml2.model.AssertionConsumerService;
import com.onelogin.saml2.settings.Saml2Settings;

/**
* Interfaced used to select the Assertion Consumer Service (ACS) to be
* specified in an authentication request. An instance of this interface can be
* passed as an input parameter in a {@link AuthnRequestParams} to be used when
* initiating a login operation.
* <p>
* A set of predefined implementations are provided: they should cover the most
* common cases.
*/
@FunctionalInterface
public interface AssertionConsumerServiceSelector {

/**
* Simple class holding data used to select an Assertion Consumer Service (ACS)
* within an authentication request.
* <p>
* The index, if specified, has priority over the pair URL/protocol binding.
*/
static class AssertionConsumerServiceSelection {
/** Assertion Consumer Service index. */
public final Integer index;
/** Assertion Consumer Service URL. */
public final URL url;
/** Assertion Consumer Service protocol binding. */
public final String protocolBinding;

/**
* Creates an Assertion Consumer Service selection by index.
*
* @param index
* the ACS index
*/
public AssertionConsumerServiceSelection(final int index) {
this.index = index;
this.url = null;
this.protocolBinding = null;
}

/**
* Creates an Assertion Consumer Service selection by URL and protocol binding.
*
* @param url
* the ACS URL
* @param protocolBinding
* the ACS protocol binding
*/
public AssertionConsumerServiceSelection(final URL url, final String protocolBinding) {
this.index = null;
this.url = url;
this.protocolBinding = protocolBinding;
}
}

/**
* @return a selector that will cause the authentication request not to specify
* any Assertion Consumer Service, letting the IdP determine which is
* the default one; if the agreement between the SP and the IdP to map
* Assertion Consumer Services is based on metadata, it means that the
* IdP is expected to select the ACS marked there as being the default
* one (or the only declared ACS, if just one exists and hopefully not
* explicitly set as <strong>not</strong> being the default one...);
* indeed, in sane cases the final selection result is expected to be
* the same the one provided by
* {@link AssertionConsumerServiceSelector#useDefaultByIndex(Saml2Settings)}
* and
* {@link AssertionConsumerServiceSelector#useDefaultByUrlAndBinding(Saml2Settings)},
* with those two however causing an explicit indication of the choice
* being made by the SP in the authentication request, indication that
* the IdP must then respect
*/
static AssertionConsumerServiceSelector useImplicitDefault() {
return () -> null;
}

/**
* @param settings
* the SAML settings, containing the list of the available
* Assertion Consumer Services (see
* {@link Saml2Settings#getSpAssertionConsumerServices()})
* @return a selector that will cause the authentication request to explicitly
* specify the default Assertion Consumer Service declared in a set of
* SAML settings, selecting it by index; if no default ACS could be
* unambiguously detected, this falls back to
* {@link #useImplicitDefault()}
* @see Saml2Settings#getSpAssertionConsumerServices()
* @see Saml2Settings#getSpDefaultAssertionConsumerService()
*/
static AssertionConsumerServiceSelector useDefaultByIndex(final Saml2Settings settings) {
return settings.getSpDefaultAssertionConsumerService().map(AssertionConsumerServiceSelector::byIndex)
.orElse(useImplicitDefault());
}

/**
* @param settings
* the SAML settings, containing the list of the available
* Assertion Consumer Services (see
* {@link Saml2Settings#getSpAssertionConsumerServices()})
* @return a selector that will cause the authentication request to explicitly
* specify the default Assertion Consumer Service declared in a set of
* SAML settings, selecting it by URL and protocol binding; if no
* default ACS could be unambiguously detected, this falls back to
* {@link #useImplicitDefault()}
* @see Saml2Settings#getSpAssertionConsumerServices()
* @see Saml2Settings#getSpDefaultAssertionConsumerService()
*/
static AssertionConsumerServiceSelector useDefaultByUrlAndBinding(final Saml2Settings settings) {
return settings.getSpDefaultAssertionConsumerService().map(AssertionConsumerServiceSelector::byUrlAndBinding)
.orElse(useImplicitDefault());
}

/**
* @param assertionConsumerService
* the Assertion Consumer Service to select
* @return a selector that chooses the specified Assertion Consumer Service by
* index
*/
static AssertionConsumerServiceSelector byIndex(final AssertionConsumerService assertionConsumerService) {
return byIndex(assertionConsumerService.getIndex());
}

/**
* @param assertionConsumerService
* the Assertion Consumer Service to select
* @return a selector that chooses the specified Assertion Consumer Service by
* location URL and protocol binding
*/
static AssertionConsumerServiceSelector byUrlAndBinding(final AssertionConsumerService assertionConsumerService) {
return () -> new AssertionConsumerServiceSelection(assertionConsumerService.getLocation(),
assertionConsumerService.getBinding());
}

/**
* @param index
* the index of the Assertion Consumer Service to select
* @return a selector that chooses the Assertion Consumer Service with the given
* index
*/
static AssertionConsumerServiceSelector byIndex(final int index) {
return () -> new AssertionConsumerServiceSelection(index);
}

/**
* @param url
* the URL of the Assertion Consumer Service to select
* @param protocolBinding
* the protocol binding of the Assertion Consumer Service to select
* @return a selector that chooses the Assertion Consumer Service with the given
* URL and protocol binding
*/
static AssertionConsumerServiceSelector byUrlAndBinding(final URL url, final String protocolBinding) {
return () -> new AssertionConsumerServiceSelection(url, protocolBinding);
}

/**
* Returns a description of the selected Assertion Consumer Service.
*
* @return the service index, or <code>null</code> if the default one should be
* selected
*/
AssertionConsumerServiceSelection getAssertionConsumerServiceSelection();
}
37 changes: 34 additions & 3 deletions core/src/main/java/com/onelogin/saml2/authn/AuthnRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.onelogin.saml2.authn.AssertionConsumerServiceSelector.AssertionConsumerServiceSelection;
import com.onelogin.saml2.model.AssertionConsumerService;
import com.onelogin.saml2.model.Organization;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.util.Constants;
Expand Down Expand Up @@ -250,8 +252,6 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
String issueInstantString = Util.formatDateTime(issueInstant.getTimeInMillis());
valueMap.put("issueInstant", issueInstantString);
valueMap.put("id", Util.toXml(String.valueOf(id)));
valueMap.put("assertionConsumerServiceURL", Util.toXml(String.valueOf(settings.getSpAssertionConsumerServiceUrl())));
valueMap.put("protocolBinding", Util.toXml(settings.getSpAssertionConsumerServiceBinding()));
valueMap.put("spEntityid", Util.toXml(settings.getSpEntityId()));

String requestedAuthnContextStr = "";
Expand All @@ -266,6 +266,37 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
}

valueMap.put("requestedAuthnContextStr", requestedAuthnContextStr);

String assertionConsumerServiceSelectionStr = "";
AssertionConsumerServiceSelection acsSelection = params.getAssertionConsumerServiceSelector()
.getAssertionConsumerServiceSelection();
List<AssertionConsumerService> spAssertionConsumerServices = settings.getSpAssertionConsumerServices();
if (spAssertionConsumerServices.size() == 1) {
/*
* For backward compatibility: if an implicit default ACS selection is
* requested, just one single ACS is defined in the settings, it has index 1
* (which was the default index used before introducing multi ACS support) and
* no explicit default status (as it was before introducing multi ACS support),
* then select that ACS by using its URL and protocol binding: indeed, the old
* way to specify the ACS in the AuhtnRequest was just this. The selected ACS
* should be the same anyway, we just ensure that, in this way, the produced
* AuthnRequest is exactly the same as it was before introducing multi ACS
* support.
*/
final AssertionConsumerService acs = spAssertionConsumerServices.get(0);
if (acsSelection == null && acs.getIndex() == 1 && acs.isDefault() == null)
acsSelection = AssertionConsumerServiceSelector.byUrlAndBinding(acs)
.getAssertionConsumerServiceSelection();
}
if (acsSelection != null) {
if (acsSelection.index != null)
assertionConsumerServiceSelectionStr = " AssertionConsumerServiceIndex=\"" + acsSelection.index
+ "\"";
else
assertionConsumerServiceSelectionStr = " ProtocolBinding=\"" + Util.toXml(acsSelection.protocolBinding)
+ "\" AssertionConsumerServiceURL=\"" + Util.toXml(String.valueOf(acsSelection.url)) + "\"";
}
valueMap.put("assertionConsumerServiceSelection", assertionConsumerServiceSelectionStr);

return new StrSubstitutor(valueMap);
}
Expand All @@ -275,7 +306,7 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
*/
private static StringBuilder getAuthnRequestTemplate() {
StringBuilder template = new StringBuilder();
template.append("<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"${id}\" Version=\"2.0\" IssueInstant=\"${issueInstant}\"${providerStr}${forceAuthnStr}${isPassiveStr}${destinationStr} ProtocolBinding=\"${protocolBinding}\" AssertionConsumerServiceURL=\"${assertionConsumerServiceURL}\">");
template.append("<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"${id}\" Version=\"2.0\" IssueInstant=\"${issueInstant}\"${providerStr}${forceAuthnStr}${isPassiveStr}${destinationStr}${assertionConsumerServiceSelection}>");
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
return template;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public class AuthnRequestParams {
*/
private final String nameIdValueReq;

/*
* Selector to use to specify the Assertion Consumer Service that will consume
* the response
*/
private final AssertionConsumerServiceSelector assertionConsumerServiceSelector;

/**
* Create a set of authentication request input parameters.
*
Expand Down Expand Up @@ -64,6 +70,27 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
this(forceAuthn, isPassive, setNameIdPolicy, allowCreate, null);
}

/**
* Create a set of authentication request input parameters.
*
* @param forceAuthn
* whether the <code>ForceAuthn</code> attribute should be set to
* <code>true</code>
* @param isPassive
* whether the <code>IsPassive</code> attribute should be set to
* <code>true</code>
* @param setNameIdPolicy
* whether a <code>NameIDPolicy</code> should be set
* @param assertionConsumerServiceSelector
* the selector to use to specify the Assertion Consumer Service
* that will consume the response; if <code>null</code>,
* {@link AssertionConsumerServiceSelector#useImplicitDefault()} is used
*/
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy,
AssertionConsumerServiceSelector assertionConsumerServiceSelector) {
this(forceAuthn, isPassive, setNameIdPolicy, true, null, assertionConsumerServiceSelector);
}

/**
* Create a set of authentication request input parameters.
*
Expand All @@ -89,7 +116,7 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
* whether the <code>ForceAuthn</code> attribute should be set to
* <code>true</code>
* @param isPassive
* whether the <code>IsPassive</code> attribute should be set to
* whether the <code>isPassive</code> attribute should be set to
* <code>true</code>
* @param setNameIdPolicy
* whether a <code>NameIDPolicy</code> should be set
Expand All @@ -103,11 +130,42 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
*/
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate,
String nameIdValueReq) {
this(forceAuthn, isPassive, setNameIdPolicy, allowCreate, nameIdValueReq, null);
}

/**
* Create a set of authentication request input parameters.
*
* @param forceAuthn
* whether the <code>ForceAuthn</code> attribute should be set to
* <code>true</code>
* @param isPassive
* whether the <code>isPassive</code> attribute should be set to
* <code>true</code>
* @param setNameIdPolicy
* whether a <code>NameIDPolicy</code> should be set
* @param allowCreate
* the value to set for the <code>allowCreate</code> attribute of
* <code>NameIDPolicy</code> element; <code>null</code> means it's
* not set at all; only meaningful when
* <code>setNameIdPolicy</code> is <code>true</code>
* @param nameIdValueReq
* the subject that should be authenticated
* @param assertionConsumerServiceSelector
* the selector to use to specify the Assertion Consumer Service
* that will consume the response; if <code>null</code>,
* {@link AssertionConsumerServiceSelector#useImplicitDefault()} is used
*/
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate, String nameIdValueReq,
AssertionConsumerServiceSelector assertionConsumerServiceSelector) {
this.forceAuthn = forceAuthn;
this.isPassive = isPassive;
this.setNameIdPolicy = setNameIdPolicy;
this.allowCreate = allowCreate;
this.nameIdValueReq = nameIdValueReq;
this.assertionConsumerServiceSelector = assertionConsumerServiceSelector != null
? assertionConsumerServiceSelector
: AssertionConsumerServiceSelector.useImplicitDefault();
}

/**
Expand All @@ -123,6 +181,7 @@ protected AuthnRequestParams(AuthnRequestParams source) {
this.setNameIdPolicy = source.isSetNameIdPolicy();
this.allowCreate = source.isAllowCreate();
this.nameIdValueReq = source.getNameIdValueReq();
this.assertionConsumerServiceSelector = source.getAssertionConsumerServiceSelector();
}

/**
Expand Down Expand Up @@ -163,4 +222,12 @@ public boolean isAllowCreate() {
public String getNameIdValueReq() {
return nameIdValueReq;
}

/**
* @return the selector to use to specify the Assertion Consumer Service that
* will consume the response
*/
public AssertionConsumerServiceSelector getAssertionConsumerServiceSelector() {
return assertionConsumerServiceSelector;
}
}
Loading

0 comments on commit 21e7b45

Please sign in to comment.