Skip to content

Commit 21e7b45

Browse files
committed
Add support for multiple Assertion Consumer Services (ACS)
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.
1 parent 043ca5e commit 21e7b45

16 files changed

+1583
-54
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,18 @@ onelogin.saml2.sp.assertion_consumer_service.url = http://localhost:8080/java-sa
224224
# HTTP-POST binding only
225225
onelogin.saml2.sp.assertion_consumer_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
226226

227+
# The above two settings declare just one Assertion Consumer Service (ACS). As an alternative, it's also
228+
# possible to specify multiple Assertion Consumer Services by providing indexed properties: the index
229+
# is used as the ACS index as well and one of the defined services may be marked as the default one.
230+
# Please note that, when indexed ACS properties are present, the non-indexed ones are ignored.
231+
# Here is a complete example, but remember that Onelogin Toolkit still actually supports HTTP-POST binding
232+
# only for response processing:
233+
#onelogin.saml2.sp.assertion_consumer_service[0].url = http://localhost:8081/java-saml-jspsample/acs1.jsp
234+
#onelogin.saml2.sp.assertion_consumer_service[0].binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
235+
#onelogin.saml2.sp.assertion_consumer_service[1].url = http://localhost:8081/java-saml-jspsample/acs2.jsp
236+
#onelogin.saml2.sp.assertion_consumer_service[1].binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
237+
#onelogin.saml2.sp.assertion_consumer_service[1].default = true
238+
227239
# Specifies info about where and how the <Logout Response> message MUST be
228240
# returned to the requester, in this case our SP.
229241
onelogin.saml2.sp.single_logout_service.url = http://localhost:8080/java-saml-tookit-jspsample/sls.jsp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package com.onelogin.saml2.authn;
2+
3+
import java.net.URL;
4+
5+
import com.onelogin.saml2.model.AssertionConsumerService;
6+
import com.onelogin.saml2.settings.Saml2Settings;
7+
8+
/**
9+
* Interfaced used to select the Assertion Consumer Service (ACS) to be
10+
* specified in an authentication request. An instance of this interface can be
11+
* passed as an input parameter in a {@link AuthnRequestParams} to be used when
12+
* initiating a login operation.
13+
* <p>
14+
* A set of predefined implementations are provided: they should cover the most
15+
* common cases.
16+
*/
17+
@FunctionalInterface
18+
public interface AssertionConsumerServiceSelector {
19+
20+
/**
21+
* Simple class holding data used to select an Assertion Consumer Service (ACS)
22+
* within an authentication request.
23+
* <p>
24+
* The index, if specified, has priority over the pair URL/protocol binding.
25+
*/
26+
static class AssertionConsumerServiceSelection {
27+
/** Assertion Consumer Service index. */
28+
public final Integer index;
29+
/** Assertion Consumer Service URL. */
30+
public final URL url;
31+
/** Assertion Consumer Service protocol binding. */
32+
public final String protocolBinding;
33+
34+
/**
35+
* Creates an Assertion Consumer Service selection by index.
36+
*
37+
* @param index
38+
* the ACS index
39+
*/
40+
public AssertionConsumerServiceSelection(final int index) {
41+
this.index = index;
42+
this.url = null;
43+
this.protocolBinding = null;
44+
}
45+
46+
/**
47+
* Creates an Assertion Consumer Service selection by URL and protocol binding.
48+
*
49+
* @param url
50+
* the ACS URL
51+
* @param protocolBinding
52+
* the ACS protocol binding
53+
*/
54+
public AssertionConsumerServiceSelection(final URL url, final String protocolBinding) {
55+
this.index = null;
56+
this.url = url;
57+
this.protocolBinding = protocolBinding;
58+
}
59+
}
60+
61+
/**
62+
* @return a selector that will cause the authentication request not to specify
63+
* any Assertion Consumer Service, letting the IdP determine which is
64+
* the default one; if the agreement between the SP and the IdP to map
65+
* Assertion Consumer Services is based on metadata, it means that the
66+
* IdP is expected to select the ACS marked there as being the default
67+
* one (or the only declared ACS, if just one exists and hopefully not
68+
* explicitly set as <strong>not</strong> being the default one...);
69+
* indeed, in sane cases the final selection result is expected to be
70+
* the same the one provided by
71+
* {@link AssertionConsumerServiceSelector#useDefaultByIndex(Saml2Settings)}
72+
* and
73+
* {@link AssertionConsumerServiceSelector#useDefaultByUrlAndBinding(Saml2Settings)},
74+
* with those two however causing an explicit indication of the choice
75+
* being made by the SP in the authentication request, indication that
76+
* the IdP must then respect
77+
*/
78+
static AssertionConsumerServiceSelector useImplicitDefault() {
79+
return () -> null;
80+
}
81+
82+
/**
83+
* @param settings
84+
* the SAML settings, containing the list of the available
85+
* Assertion Consumer Services (see
86+
* {@link Saml2Settings#getSpAssertionConsumerServices()})
87+
* @return a selector that will cause the authentication request to explicitly
88+
* specify the default Assertion Consumer Service declared in a set of
89+
* SAML settings, selecting it by index; if no default ACS could be
90+
* unambiguously detected, this falls back to
91+
* {@link #useImplicitDefault()}
92+
* @see Saml2Settings#getSpAssertionConsumerServices()
93+
* @see Saml2Settings#getSpDefaultAssertionConsumerService()
94+
*/
95+
static AssertionConsumerServiceSelector useDefaultByIndex(final Saml2Settings settings) {
96+
return settings.getSpDefaultAssertionConsumerService().map(AssertionConsumerServiceSelector::byIndex)
97+
.orElse(useImplicitDefault());
98+
}
99+
100+
/**
101+
* @param settings
102+
* the SAML settings, containing the list of the available
103+
* Assertion Consumer Services (see
104+
* {@link Saml2Settings#getSpAssertionConsumerServices()})
105+
* @return a selector that will cause the authentication request to explicitly
106+
* specify the default Assertion Consumer Service declared in a set of
107+
* SAML settings, selecting it by URL and protocol binding; if no
108+
* default ACS could be unambiguously detected, this falls back to
109+
* {@link #useImplicitDefault()}
110+
* @see Saml2Settings#getSpAssertionConsumerServices()
111+
* @see Saml2Settings#getSpDefaultAssertionConsumerService()
112+
*/
113+
static AssertionConsumerServiceSelector useDefaultByUrlAndBinding(final Saml2Settings settings) {
114+
return settings.getSpDefaultAssertionConsumerService().map(AssertionConsumerServiceSelector::byUrlAndBinding)
115+
.orElse(useImplicitDefault());
116+
}
117+
118+
/**
119+
* @param assertionConsumerService
120+
* the Assertion Consumer Service to select
121+
* @return a selector that chooses the specified Assertion Consumer Service by
122+
* index
123+
*/
124+
static AssertionConsumerServiceSelector byIndex(final AssertionConsumerService assertionConsumerService) {
125+
return byIndex(assertionConsumerService.getIndex());
126+
}
127+
128+
/**
129+
* @param assertionConsumerService
130+
* the Assertion Consumer Service to select
131+
* @return a selector that chooses the specified Assertion Consumer Service by
132+
* location URL and protocol binding
133+
*/
134+
static AssertionConsumerServiceSelector byUrlAndBinding(final AssertionConsumerService assertionConsumerService) {
135+
return () -> new AssertionConsumerServiceSelection(assertionConsumerService.getLocation(),
136+
assertionConsumerService.getBinding());
137+
}
138+
139+
/**
140+
* @param index
141+
* the index of the Assertion Consumer Service to select
142+
* @return a selector that chooses the Assertion Consumer Service with the given
143+
* index
144+
*/
145+
static AssertionConsumerServiceSelector byIndex(final int index) {
146+
return () -> new AssertionConsumerServiceSelection(index);
147+
}
148+
149+
/**
150+
* @param url
151+
* the URL of the Assertion Consumer Service to select
152+
* @param protocolBinding
153+
* the protocol binding of the Assertion Consumer Service to select
154+
* @return a selector that chooses the Assertion Consumer Service with the given
155+
* URL and protocol binding
156+
*/
157+
static AssertionConsumerServiceSelector byUrlAndBinding(final URL url, final String protocolBinding) {
158+
return () -> new AssertionConsumerServiceSelection(url, protocolBinding);
159+
}
160+
161+
/**
162+
* Returns a description of the selected Assertion Consumer Service.
163+
*
164+
* @return the service index, or <code>null</code> if the default one should be
165+
* selected
166+
*/
167+
AssertionConsumerServiceSelection getAssertionConsumerServiceSelection();
168+
}

core/src/main/java/com/onelogin/saml2/authn/AuthnRequest.java

+34-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.slf4j.Logger;
1212
import org.slf4j.LoggerFactory;
1313

14+
import com.onelogin.saml2.authn.AssertionConsumerServiceSelector.AssertionConsumerServiceSelection;
15+
import com.onelogin.saml2.model.AssertionConsumerService;
1416
import com.onelogin.saml2.model.Organization;
1517
import com.onelogin.saml2.settings.Saml2Settings;
1618
import com.onelogin.saml2.util.Constants;
@@ -250,8 +252,6 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
250252
String issueInstantString = Util.formatDateTime(issueInstant.getTimeInMillis());
251253
valueMap.put("issueInstant", issueInstantString);
252254
valueMap.put("id", Util.toXml(String.valueOf(id)));
253-
valueMap.put("assertionConsumerServiceURL", Util.toXml(String.valueOf(settings.getSpAssertionConsumerServiceUrl())));
254-
valueMap.put("protocolBinding", Util.toXml(settings.getSpAssertionConsumerServiceBinding()));
255255
valueMap.put("spEntityid", Util.toXml(settings.getSpEntityId()));
256256

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

268268
valueMap.put("requestedAuthnContextStr", requestedAuthnContextStr);
269+
270+
String assertionConsumerServiceSelectionStr = "";
271+
AssertionConsumerServiceSelection acsSelection = params.getAssertionConsumerServiceSelector()
272+
.getAssertionConsumerServiceSelection();
273+
List<AssertionConsumerService> spAssertionConsumerServices = settings.getSpAssertionConsumerServices();
274+
if (spAssertionConsumerServices.size() == 1) {
275+
/*
276+
* For backward compatibility: if an implicit default ACS selection is
277+
* requested, just one single ACS is defined in the settings, it has index 1
278+
* (which was the default index used before introducing multi ACS support) and
279+
* no explicit default status (as it was before introducing multi ACS support),
280+
* then select that ACS by using its URL and protocol binding: indeed, the old
281+
* way to specify the ACS in the AuhtnRequest was just this. The selected ACS
282+
* should be the same anyway, we just ensure that, in this way, the produced
283+
* AuthnRequest is exactly the same as it was before introducing multi ACS
284+
* support.
285+
*/
286+
final AssertionConsumerService acs = spAssertionConsumerServices.get(0);
287+
if (acsSelection == null && acs.getIndex() == 1 && acs.isDefault() == null)
288+
acsSelection = AssertionConsumerServiceSelector.byUrlAndBinding(acs)
289+
.getAssertionConsumerServiceSelection();
290+
}
291+
if (acsSelection != null) {
292+
if (acsSelection.index != null)
293+
assertionConsumerServiceSelectionStr = " AssertionConsumerServiceIndex=\"" + acsSelection.index
294+
+ "\"";
295+
else
296+
assertionConsumerServiceSelectionStr = " ProtocolBinding=\"" + Util.toXml(acsSelection.protocolBinding)
297+
+ "\" AssertionConsumerServiceURL=\"" + Util.toXml(String.valueOf(acsSelection.url)) + "\"";
298+
}
299+
valueMap.put("assertionConsumerServiceSelection", assertionConsumerServiceSelectionStr);
269300

270301
return new StrSubstitutor(valueMap);
271302
}
@@ -275,7 +306,7 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
275306
*/
276307
private static StringBuilder getAuthnRequestTemplate() {
277308
StringBuilder template = new StringBuilder();
278-
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}\">");
309+
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}>");
279310
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
280311
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
281312
return template;

core/src/main/java/com/onelogin/saml2/authn/AuthnRequestParams.java

+68-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public class AuthnRequestParams {
2727
*/
2828
private final String nameIdValueReq;
2929

30+
/*
31+
* Selector to use to specify the Assertion Consumer Service that will consume
32+
* the response
33+
*/
34+
private final AssertionConsumerServiceSelector assertionConsumerServiceSelector;
35+
3036
/**
3137
* Create a set of authentication request input parameters.
3238
*
@@ -64,6 +70,27 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
6470
this(forceAuthn, isPassive, setNameIdPolicy, allowCreate, null);
6571
}
6672

73+
/**
74+
* Create a set of authentication request input parameters.
75+
*
76+
* @param forceAuthn
77+
* whether the <code>ForceAuthn</code> attribute should be set to
78+
* <code>true</code>
79+
* @param isPassive
80+
* whether the <code>IsPassive</code> attribute should be set to
81+
* <code>true</code>
82+
* @param setNameIdPolicy
83+
* whether a <code>NameIDPolicy</code> should be set
84+
* @param assertionConsumerServiceSelector
85+
* the selector to use to specify the Assertion Consumer Service
86+
* that will consume the response; if <code>null</code>,
87+
* {@link AssertionConsumerServiceSelector#useImplicitDefault()} is used
88+
*/
89+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy,
90+
AssertionConsumerServiceSelector assertionConsumerServiceSelector) {
91+
this(forceAuthn, isPassive, setNameIdPolicy, true, null, assertionConsumerServiceSelector);
92+
}
93+
6794
/**
6895
* Create a set of authentication request input parameters.
6996
*
@@ -89,7 +116,7 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
89116
* whether the <code>ForceAuthn</code> attribute should be set to
90117
* <code>true</code>
91118
* @param isPassive
92-
* whether the <code>IsPassive</code> attribute should be set to
119+
* whether the <code>isPassive</code> attribute should be set to
93120
* <code>true</code>
94121
* @param setNameIdPolicy
95122
* whether a <code>NameIDPolicy</code> should be set
@@ -103,11 +130,42 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
103130
*/
104131
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate,
105132
String nameIdValueReq) {
133+
this(forceAuthn, isPassive, setNameIdPolicy, allowCreate, nameIdValueReq, null);
134+
}
135+
136+
/**
137+
* Create a set of authentication request input parameters.
138+
*
139+
* @param forceAuthn
140+
* whether the <code>ForceAuthn</code> attribute should be set to
141+
* <code>true</code>
142+
* @param isPassive
143+
* whether the <code>isPassive</code> attribute should be set to
144+
* <code>true</code>
145+
* @param setNameIdPolicy
146+
* whether a <code>NameIDPolicy</code> should be set
147+
* @param allowCreate
148+
* the value to set for the <code>allowCreate</code> attribute of
149+
* <code>NameIDPolicy</code> element; <code>null</code> means it's
150+
* not set at all; only meaningful when
151+
* <code>setNameIdPolicy</code> is <code>true</code>
152+
* @param nameIdValueReq
153+
* the subject that should be authenticated
154+
* @param assertionConsumerServiceSelector
155+
* the selector to use to specify the Assertion Consumer Service
156+
* that will consume the response; if <code>null</code>,
157+
* {@link AssertionConsumerServiceSelector#useImplicitDefault()} is used
158+
*/
159+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate, String nameIdValueReq,
160+
AssertionConsumerServiceSelector assertionConsumerServiceSelector) {
106161
this.forceAuthn = forceAuthn;
107162
this.isPassive = isPassive;
108163
this.setNameIdPolicy = setNameIdPolicy;
109164
this.allowCreate = allowCreate;
110165
this.nameIdValueReq = nameIdValueReq;
166+
this.assertionConsumerServiceSelector = assertionConsumerServiceSelector != null
167+
? assertionConsumerServiceSelector
168+
: AssertionConsumerServiceSelector.useImplicitDefault();
111169
}
112170

113171
/**
@@ -123,6 +181,7 @@ protected AuthnRequestParams(AuthnRequestParams source) {
123181
this.setNameIdPolicy = source.isSetNameIdPolicy();
124182
this.allowCreate = source.isAllowCreate();
125183
this.nameIdValueReq = source.getNameIdValueReq();
184+
this.assertionConsumerServiceSelector = source.getAssertionConsumerServiceSelector();
126185
}
127186

128187
/**
@@ -163,4 +222,12 @@ public boolean isAllowCreate() {
163222
public String getNameIdValueReq() {
164223
return nameIdValueReq;
165224
}
225+
226+
/**
227+
* @return the selector to use to specify the Assertion Consumer Service that
228+
* will consume the response
229+
*/
230+
public AssertionConsumerServiceSelector getAssertionConsumerServiceSelector() {
231+
return assertionConsumerServiceSelector;
232+
}
166233
}

0 commit comments

Comments
 (0)