Skip to content

Commit 54a1459

Browse files
committed
Add support for multiple Assertion Consumer Services (ACS)
The approach is very similar to the one adopted to implement support for multiple Attribute Consuming Services and it 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.
1 parent b707ea9 commit 54a1459

16 files changed

Lines changed: 1267 additions & 64 deletions

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@ onelogin.saml2.sp.assertion_consumer_service.url = http://localhost:8080/java-sa
214214
# HTTP-POST binding only
215215
onelogin.saml2.sp.assertion_consumer_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
216216

217+
# The above two settings declare just one Assertion Consumer Service (ACS). As an alternative, it's also
218+
# possible to specify multiple Assertion Consumer Services by providing indexed properties: the index
219+
# is used as the ACS index as well and one of the defined services may be marked as the default one.
220+
# Please note that, when indexed ACS properties are present, the non-indexed ones are ignored.
221+
# Here is a complete example, but remember that Onelogin Toolkit still actually supports HTTP-POST binding
222+
# only for response processing:
223+
#onelogin.saml2.sp.assertion_consumer_service[0].url = http://localhost:8081/java-saml-jspsample/acs1.jsp
224+
#onelogin.saml2.sp.assertion_consumer_service[0].binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
225+
#onelogin.saml2.sp.assertion_consumer_service[1].url = http://localhost:8081/java-saml-jspsample/acs2.jsp
226+
#onelogin.saml2.sp.assertion_consumer_service[1].binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
227+
#onelogin.saml2.sp.assertion_consumer_service[1].default = true
228+
217229
# Specifies info about where and how the <Logout Response> message MUST be
218230
# returned to the requester, in this case our SP.
219231
onelogin.saml2.sp.single_logout_service.url = http://localhost:8080/java-saml-tookit-jspsample/sls.jsp
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.onelogin.saml2.authn;
2+
3+
import java.net.URL;
4+
5+
import com.onelogin.saml2.model.AssertionConsumerService;
6+
7+
/**
8+
* Interfaced used to select the Assertion Consumer Service (ACS) to be
9+
* specified in an authentication request. An instance of this interface can be
10+
* passed as an input parameter in a {@link AuthnRequestParams} to be used when
11+
* initiating a login operation.
12+
* <p>
13+
* A set of predefined implementations are provided: they should cover the most
14+
* common cases.
15+
*/
16+
@FunctionalInterface
17+
public interface AssertionConsumerServiceSelector {
18+
19+
/**
20+
* Simple class holding data used to select an Assertion Consumer Service (ACS)
21+
* within an authentication request.
22+
* <p>
23+
* The index, if specified, has priority over the pair URL/protocol binding.
24+
*/
25+
static class AssertionConsumerServiceSelection {
26+
/** Assertion Consumer Service index. */
27+
public final Integer index;
28+
/** Assertion Consumer Service URL. */
29+
public final URL url;
30+
/** Assertion Consumer Service protocol binding. */
31+
public final String protocolBinding;
32+
33+
/**
34+
* Creates an Assertion Consumer Service selection by index.
35+
*
36+
* @param index
37+
* the ACS index
38+
*/
39+
public AssertionConsumerServiceSelection(final int index) {
40+
this.index = index;
41+
this.url = null;
42+
this.protocolBinding = null;
43+
}
44+
45+
/**
46+
* Creates an Assertion Consumer Service selection by URL and protocol binding.
47+
*
48+
* @param url
49+
* the ACS URL
50+
* @param protocolBinding
51+
* the ACS protocol binding
52+
*/
53+
public AssertionConsumerServiceSelection(final URL url, final String protocolBinding) {
54+
this.index = null;
55+
this.url = url;
56+
this.protocolBinding = protocolBinding;
57+
}
58+
}
59+
60+
/**
61+
* @return a selector of the default Assertion Consumer Service
62+
*/
63+
static AssertionConsumerServiceSelector useDefault() {
64+
return () -> null;
65+
}
66+
67+
/**
68+
* @param assertionConsumerService
69+
* the Assertion Consumer Service to select
70+
* @return a selector the chooses the specified Assertion Consumer Service by
71+
* index
72+
*/
73+
static AssertionConsumerServiceSelector byIndex(final AssertionConsumerService assertionConsumerService) {
74+
return byIndex(assertionConsumerService.getIndex());
75+
}
76+
77+
/**
78+
* @param assertionConsumerService
79+
* the Assertion Consumer Service to select
80+
* @return a selector the chooses the specified Assertion Consumer Service by location URL and protocol
81+
* binding
82+
*/
83+
static AssertionConsumerServiceSelector byUrlAndBinding(final AssertionConsumerService assertionConsumerService) {
84+
return () -> new AssertionConsumerServiceSelection(assertionConsumerService.getLocation(), assertionConsumerService.getBinding());
85+
}
86+
87+
/**
88+
* @param index
89+
* the index of the Assertion Consumer Service to select
90+
* @return a selector that chooses the Assertion Consumer Service with the given
91+
* index
92+
*/
93+
static AssertionConsumerServiceSelector byIndex(final int index) {
94+
return () -> new AssertionConsumerServiceSelection(index);
95+
}
96+
97+
/**
98+
* @param url
99+
* the URL of the Assertion Consumer Service to select
100+
* @param protocolBinding
101+
* the protocol binding of the Assertion Consumer Service to select
102+
* @return a selector that chooses the Assertion Consumer Service with the given
103+
* URL and protocol binding
104+
*/
105+
static AssertionConsumerServiceSelector byUrlAndBinding(final URL url, final String protocolBinding) {
106+
return () -> new AssertionConsumerServiceSelection(url, protocolBinding);
107+
}
108+
109+
/**
110+
* Returns a description of the selected Assertion Consumer Service.
111+
*
112+
* @return the service index, or <code>null</code> if the default one should be
113+
* selected
114+
*/
115+
AssertionConsumerServiceSelection getAssertionConsumerServiceSelection();
116+
}

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

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import org.slf4j.LoggerFactory;
1313

1414
import com.onelogin.saml2.settings.Saml2Settings;
15+
import com.onelogin.saml2.authn.AssertionConsumerServiceSelector.AssertionConsumerServiceSelection;
16+
import com.onelogin.saml2.model.AssertionConsumerService;
1517
import com.onelogin.saml2.model.Organization;
1618
import com.onelogin.saml2.util.Constants;
1719
import com.onelogin.saml2.util.Util;
@@ -241,8 +243,6 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
241243
String issueInstantString = Util.formatDateTime(issueInstant.getTimeInMillis());
242244
valueMap.put("issueInstant", issueInstantString);
243245
valueMap.put("id", Util.toXml(String.valueOf(id)));
244-
valueMap.put("assertionConsumerServiceURL", Util.toXml(String.valueOf(settings.getSpAssertionConsumerServiceUrl())));
245-
valueMap.put("protocolBinding", Util.toXml(settings.getSpAssertionConsumerServiceBinding()));
246246
valueMap.put("spEntityid", Util.toXml(settings.getSpEntityId()));
247247

248248
String requestedAuthnContextStr = "";
@@ -258,10 +258,37 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
258258

259259
valueMap.put("requestedAuthnContextStr", requestedAuthnContextStr);
260260

261+
String assertionConsumerServiceSelectionStr = "";
262+
AssertionConsumerServiceSelection acsSelection = getAssertionConsumerServiceSelector()
263+
.getAssertionConsumerServiceSelection();
264+
List<AssertionConsumerService> spAssertionConsumerServices = settings.getSpAssertionConsumerServices();
265+
if (spAssertionConsumerServices.size() == 1) {
266+
/*
267+
* For backward compatibility: when just one single ACS is defined in the
268+
* settings and it has index 1 (which was the default index used before
269+
* introducing multi ACS support), then select the ACS by using the URL and
270+
* binding of that ACS: indeed, the old way to specify the ACS in the
271+
* AuhtnRequest was just this.
272+
*/
273+
final AssertionConsumerService acs = spAssertionConsumerServices.get(0);
274+
if (acsSelection == null && acs.getIndex() == 1)
275+
acsSelection = AssertionConsumerServiceSelector.byUrlAndBinding(acs)
276+
.getAssertionConsumerServiceSelection();
277+
}
278+
if (acsSelection != null) {
279+
if (acsSelection.index != null)
280+
assertionConsumerServiceSelectionStr = " AssertionConsumerServiceIndex=\"" + acsSelection.index
281+
+ "\"";
282+
else
283+
assertionConsumerServiceSelectionStr = " ProtocolBinding=\"" + Util.toXml(acsSelection.protocolBinding)
284+
+ "\" AssertionConsumerServiceURL=\"" + Util.toXml(String.valueOf(acsSelection.url)) + "\"";
285+
}
286+
valueMap.put("assertionConsumerServiceSelection", assertionConsumerServiceSelectionStr);
287+
261288
String attributeConsumingServiceIndexStr = "";
262-
final Integer acsIndex = getAttributeConsumingServiceSelector().getAttributeConsumingServiceIndex();
263-
if (acsIndex != null)
264-
attributeConsumingServiceIndexStr = " AttributeConsumingServiceIndex=\"" + acsIndex + "\"";
289+
final Integer attributeConsumingServiceIndex = getAttributeConsumingServiceSelector().getAttributeConsumingServiceIndex();
290+
if (attributeConsumingServiceIndex != null)
291+
attributeConsumingServiceIndexStr = " AttributeConsumingServiceIndex=\"" + attributeConsumingServiceIndex + "\"";
265292
valueMap.put("attributeConsumingServiceIndexStr", attributeConsumingServiceIndexStr);
266293

267294
return new StrSubstitutor(valueMap);
@@ -272,7 +299,7 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
272299
*/
273300
private static StringBuilder getAuthnRequestTemplate() {
274301
StringBuilder template = new StringBuilder();
275-
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}${attributeConsumingServiceIndexStr}\">");
302+
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}${attributeConsumingServiceIndexStr}\">");
276303
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
277304
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
278305
return template;

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

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ public class AuthnRequestParams {
2323
private final String nameIdValueReq;
2424

2525
/*
26-
* / Selector to use to specify the Attribute Consuming Service index
26+
* Selector to use to specify the Assertion Consumer Service that will consume
27+
* the response
2728
*/
28-
private AttributeConsumingServiceSelector attributeConsumingServiceSelector;
29+
private final AssertionConsumerServiceSelector assertionConsumerServiceSelector;
30+
31+
/*
32+
* Selector to use to specify the Attribute Consuming Service index
33+
*/
34+
private final AttributeConsumingServiceSelector attributeConsumingServiceSelector;
2935

3036
/**
3137
* Create a set of authentication request input parameters. The
@@ -42,7 +48,7 @@ public class AuthnRequestParams {
4248
* whether a <code>NameIDPolicy</code> should be set
4349
*/
4450
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy) {
45-
this(forceAuthn, isPassive, setNameIdPolicy, null, null);
51+
this(forceAuthn, isPassive, setNameIdPolicy, null, null, null);
4652
}
4753

4854
/**
@@ -62,7 +68,28 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
6268
* the subject that should be authenticated
6369
*/
6470
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq) {
65-
this(forceAuthn, isPassive, setNameIdPolicy, nameIdValueReq, null);
71+
this(forceAuthn, isPassive, setNameIdPolicy, nameIdValueReq, null, null);
72+
}
73+
74+
/**
75+
* Create a set of authentication request input parameters.
76+
*
77+
* @param forceAuthn
78+
* whether the <code>ForceAuthn</code> attribute should be set to
79+
* <code>true</code>
80+
* @param isPassive
81+
* whether the <code>isPassive</code> attribute should be set to
82+
* <code>true</code>
83+
* @param setNameIdPolicy
84+
* whether a <code>NameIDPolicy</code> should be set
85+
* @param assertionConsumerServiceSelector
86+
* the selector to use to specify the Assertion Consumer Service
87+
* that will consume the response; if <code>null</code>,
88+
* {@link AssertionConsumerServiceSelector#useDefault()} is used
89+
*/
90+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy,
91+
AssertionConsumerServiceSelector assertionConsumerServiceSelector) {
92+
this(forceAuthn, isPassive, setNameIdPolicy, null, assertionConsumerServiceSelector);
6693
}
6794

6895
/**
@@ -99,17 +126,71 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
99126
* whether a <code>NameIDPolicy</code> should be set
100127
* @param nameIdValueReq
101128
* the subject that should be authenticated
129+
* @param assertionConsumerServiceSelector
130+
* the selector to use to specify the Assertion Consumer Service
131+
* that will consume the response; if <code>null</code>,
132+
* {@link AssertionConsumerServiceSelector#useDefault()} is used
133+
*/
134+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq,
135+
AssertionConsumerServiceSelector assertionConsumerServiceSelector) {
136+
this(forceAuthn, isPassive, setNameIdPolicy, nameIdValueReq, assertionConsumerServiceSelector, null);
137+
}
138+
139+
/**
140+
* Create a set of authentication request input parameters.
141+
*
142+
* @param forceAuthn
143+
* whether the <code>ForceAuthn</code> attribute should be set to
144+
* <code>true</code>
145+
* @param isPassive
146+
* whether the <code>isPassive</code> attribute should be set to
147+
* <code>true</code>
148+
* @param setNameIdPolicy
149+
* whether a <code>NameIDPolicy</code> should be set
150+
* @param nameIdValueReq
151+
* the subject that should be authenticated
152+
* @param attributeConsumingServiceSelector
153+
* the selector to use to specify the Attribute Consuming Service
154+
* index; if <code>null</code>,
155+
* {@link AttributeConsumingServiceSelector#useDefault()} is used
156+
*/
157+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq,
158+
AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
159+
this(forceAuthn, isPassive, setNameIdPolicy, nameIdValueReq, null, attributeConsumingServiceSelector);
160+
}
161+
162+
/**
163+
* Create a set of authentication request input parameters.
164+
*
165+
* @param forceAuthn
166+
* whether the <code>ForceAuthn</code> attribute should be set to
167+
* <code>true</code>
168+
* @param isPassive
169+
* whether the <code>isPassive</code> attribute should be set to
170+
* <code>true</code>
171+
* @param setNameIdPolicy
172+
* whether a <code>NameIDPolicy</code> should be set
173+
* @param nameIdValueReq
174+
* the subject that should be authenticated
175+
* @param assertionConsumerServiceSelector
176+
* the selector to use to specify the Assertion Consumer Service
177+
* that will consume the response; if <code>null</code>,
178+
* {@link AssertionConsumerServiceSelector#useDefault()} is used
102179
* @param attributeConsumingServiceSelector
103180
* the selector to use to specify the Attribute Consuming Service
104181
* index; if <code>null</code>,
105182
* {@link AttributeConsumingServiceSelector#useDefault()} is used
106183
*/
107184
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq,
185+
AssertionConsumerServiceSelector assertionConsumerServiceSelector,
108186
AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
109187
this.forceAuthn = forceAuthn;
110188
this.isPassive = isPassive;
111189
this.setNameIdPolicy = setNameIdPolicy;
112190
this.nameIdValueReq = nameIdValueReq;
191+
this.assertionConsumerServiceSelector = assertionConsumerServiceSelector != null
192+
? assertionConsumerServiceSelector
193+
: AssertionConsumerServiceSelector.useDefault();
113194
this.attributeConsumingServiceSelector = attributeConsumingServiceSelector != null
114195
? attributeConsumingServiceSelector
115196
: AttributeConsumingServiceSelector.useDefault();
@@ -127,6 +208,7 @@ protected AuthnRequestParams(AuthnRequestParams source) {
127208
this.isPassive = source.isPassive();
128209
this.setNameIdPolicy = source.isSetNameIdPolicy();
129210
this.nameIdValueReq = source.getNameIdValueReq();
211+
this.assertionConsumerServiceSelector = source.getAssertionConsumerServiceSelector();
130212
this.attributeConsumingServiceSelector = source.getAttributeConsumingServiceSelector();
131213
}
132214

@@ -166,4 +248,12 @@ protected String getNameIdValueReq() {
166248
protected AttributeConsumingServiceSelector getAttributeConsumingServiceSelector() {
167249
return attributeConsumingServiceSelector;
168250
}
251+
252+
/**
253+
* @return the selector to use to specify the Assertion Consumer Service that
254+
* will consume the response
255+
*/
256+
protected AssertionConsumerServiceSelector getAssertionConsumerServiceSelector() {
257+
return assertionConsumerServiceSelector;
258+
}
169259
}

0 commit comments

Comments
 (0)