Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple Assertion Consumer Services (ACS) #368

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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