Skip to content

Commit 8b23f30

Browse files
committed
Add support for multiple Attribute Consuming Services
Attribute Consuming Services can now be configured in settings. They will then be parsed and added to the generated metadata. On SSO initiation a selector mechanism can be used to select the desired ACS, producing the proper AttributeConsumingServiceIndex attribute in the AuthnRequest.
1 parent 043ca5e commit 8b23f30

14 files changed

+1300
-72
lines changed

README.md

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ onelogin.saml2.sp.x509certNew =
250250
# If you have PKCS#1 BEGIN RSA PRIVATE KEY convert it by openssl pkcs8 -topk8 -inform pem -nocrypt -in sp.rsa_key -outform pem -out sp.pem
251251
onelogin.saml2.sp.privatekey =
252252

253+
# Attribute Consuming Services
254+
# SEE BELOW
255+
253256
## Identity Provider Data that we want connect with our SP ##
254257

255258
# Identifier of the IdP entity (must be a URI)
@@ -497,8 +500,87 @@ The getSPMetadata will return the metadata signed or not based on the security p
497500

498501
Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid.
499502

500-
##### Attribute Consumer Service(ACS)
501-
This code handles the SAML response that the IdP forwards to the SP through the user's client.
503+
##### Attribute Consuming Service (ACS)
504+
The SP may optionally specify one or more Attribute Consuming Services in its metadata. These can be configured in the settings.
505+
506+
If just one ACS is required:
507+
508+
```properties
509+
# Attribute Consuming Service name when just one ACS should be declared by the SP.
510+
# Comment out or set to empty if no ACS should be declared, or if multiple ones should (see below).
511+
# The service name is mandatory.
512+
onelogin.saml2.sp.attribute_consuming_service.name = My service
513+
514+
# Attribute Consuming Service description when just one ACS should be declared by the SP.
515+
# Ignored if the previous property is commented or empty.
516+
# The service description is optional.
517+
onelogin.saml2.sp.attribute_consuming_service.description = My service description
518+
519+
# Language used for Attribute Consuming Service name and description when just one ACS should be declared by the SP.
520+
# Ignored if the name property is commented or empty.
521+
# The language is optional and default to "en" (English).
522+
onelogin.saml2.sp.attribute_consuming_service.lang = en
523+
524+
# Requested attributes to be included in the Attribute Consuming Service when just one ACS should be declared by the SP.
525+
# At least one requested attribute must be specified, otherwise schema validation will fail.
526+
# Attribute properties are indexed properties, starting from 0. The index is used only to enumerate and sort attributes, but it's required.
527+
# The following properties allow to define each requested attribute:
528+
# - name: mandatory
529+
# - name_format: optional; if omitted, defaults to urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified
530+
# - friendly_name: optional; if omitted, it won't appear in SP metadata
531+
# - required: optional; if omitted or empty, defaults to false
532+
# - value[x]: an attribute value; the [x] is only used only to enumerate and sort values, but it's required
533+
# Please note that only simple values are currently supported and treated internally as strings. Hence no structured values
534+
# and no ability to specify an xsi:type attribute.
535+
# Attribute values are optional and most often they are simply omitted.
536+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name = Email
537+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
538+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].friendly_name = E-mail address
539+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].required = true
540+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[0] = foo@example.org
541+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[1] = bar@example.org
542+
```
543+
544+
If multiple ACSs are required, they can be specified in a similar way, but using indexes: these indexes are used to enumerate and
545+
identify attribute consuming services within the SP metadata and can be subsequently used in the auth process to specify which
546+
attribute set should be requested to the IdP. The "default" property can also be set to designate the default ACS. Here is an example:
547+
548+
```properties
549+
onelogin.saml2.sp.attribute_consuming_service[0].name = Just e-mail
550+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name = Email
551+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
552+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].friendly_name = E-mail address
553+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].required = true
554+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[0] = foo@example.org
555+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[1] = bar@example.org
556+
onelogin.saml2.sp.attribute_consuming_service[1].name = Anagrafica
557+
onelogin.saml2.sp.attribute_consuming_service[1].description = Set completo
558+
onelogin.saml2.sp.attribute_consuming_service[1].lang = it
559+
onelogin.saml2.sp.attribute_consuming_service[1].default = true
560+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[0].name = FirstName
561+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].name = LastName
562+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].required = true
563+
```
564+
565+
Please note that if you specify (multiple) indexed Attribute Consuming Services, the non-indexed properties will be ignored.
566+
567+
As said, to request a specific attribute set when initiating SSO, a selection mechanism is available:
568+
569+
```java
570+
import static com.onelogin.saml2.authn.AttributeConsumingServiceSelector.*;
571+
Auth auth = new Auth(request, response);
572+
// select by index 1
573+
auth.login(new AuthnRequestParams(false, false, true, byIndex(1));
574+
// or select by ACS name
575+
auth.login(new AuthnRequestParams(false, false, true, byServiceName(auth.getSettings(), "Anagrafica"));
576+
// or see AttributeConsumingServiceSelector interface implementations for more options
577+
```
578+
579+
If no selector is specified, `AttributeConsumingServiceSelector.useDefault()` will be used, which will simply omit any
580+
`AttributeConsumingServiceIndex` from the request, hence leaving the IdP choose the default attribute set agreed upon.
581+
582+
Then, the following code handles the SAML response that the IdP forwards to the SP through the user's client:
583+
502584
```java
503585
Auth auth = new Auth(request, response);
504586
auth.processResponse();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.onelogin.saml2.authn;
2+
3+
import java.util.List;
4+
5+
import com.onelogin.saml2.model.AttributeConsumingService;
6+
import com.onelogin.saml2.settings.Saml2Settings;
7+
8+
/**
9+
* Interfaced used to select the Attribute Consuming Service to be specified in
10+
* an authentication request. An instance of this interface can be passed as an
11+
* input parameter in a {@link AuthnRequestParams} to be used when initiating a
12+
* 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 AttributeConsumingServiceSelector {
19+
20+
/**
21+
* @return a selector of the default Attribute Consuming Service
22+
*/
23+
static AttributeConsumingServiceSelector useDefault() {
24+
return () -> null;
25+
}
26+
27+
/**
28+
* @param attributeConsumingService
29+
* the Attribute Consuming Service to select
30+
* @return a selector the chooses the specified Attribute Consuming Service;
31+
* indeed, its index is used
32+
*/
33+
static AttributeConsumingServiceSelector use(final AttributeConsumingService attributeConsumingService) {
34+
return byIndex(attributeConsumingService.getIndex());
35+
}
36+
37+
/**
38+
* @param index
39+
* the index of the Attribute Consuming Service to select
40+
* @return a selector that chooses the Attribute Consuming Service with the
41+
* given index
42+
*/
43+
static AttributeConsumingServiceSelector byIndex(final int index) {
44+
return () -> index;
45+
}
46+
47+
/**
48+
* @param settings
49+
* the SAML settings, containing the list of the available
50+
* Attribute Consuming Services (see
51+
* {@link Saml2Settings#getSpAttributeConsumingServices()})
52+
* @param serviceName
53+
* the name of the Attribute Consuming Service to select
54+
* @return a selector that chooses the Attribute Consuming Service with the
55+
* given name; please note that this selector will select the default
56+
* service if no one is found with the given name
57+
*/
58+
static AttributeConsumingServiceSelector byServiceName(final Saml2Settings settings, final String serviceName) {
59+
return () -> {
60+
final List<AttributeConsumingService> services = settings.getSpAttributeConsumingServices();
61+
if (services != null)
62+
return services.stream().filter(service -> service.getServiceName().equals(serviceName))
63+
.findFirst().map(AttributeConsumingService::getIndex).orElse(null);
64+
else
65+
return null;
66+
};
67+
}
68+
69+
/**
70+
* Returns the index of the selected Attribute Consuming Service.
71+
*
72+
* @return the service index, or <code>null</code> if the default one should be selected
73+
*/
74+
Integer getAttributeConsumingServiceIndex();
75+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
266266
}
267267

268268
valueMap.put("requestedAuthnContextStr", requestedAuthnContextStr);
269+
270+
String attributeConsumingServiceIndexStr = "";
271+
final Integer acsIndex = params.getAttributeConsumingServiceSelector().getAttributeConsumingServiceIndex();
272+
if (acsIndex != null)
273+
attributeConsumingServiceIndexStr = " AttributeConsumingServiceIndex=\"" + acsIndex + "\"";
274+
valueMap.put("attributeConsumingServiceIndexStr", attributeConsumingServiceIndexStr);
269275

270276
return new StrSubstitutor(valueMap);
271277
}
@@ -275,7 +281,7 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
275281
*/
276282
private static StringBuilder getAuthnRequestTemplate() {
277283
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}\">");
284+
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}\">");
279285
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
280286
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
281287
return template;

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

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,15 @@ public class AuthnRequestParams {
2727
*/
2828
private final String nameIdValueReq;
2929

30+
/*
31+
* / Selector to use to specify the Attribute Consuming Service index
32+
*/
33+
private AttributeConsumingServiceSelector attributeConsumingServiceSelector;
34+
3035
/**
31-
* Create a set of authentication request input parameters.
36+
* Create a set of authentication request input parameters. The
37+
* {@link AttributeConsumingServiceSelector#useDefault()} selector is used to
38+
* select the Attribute Consuming Service.
3239
*
3340
* @param forceAuthn
3441
* whether the <code>ForceAuthn</code> attribute should be set to
@@ -71,6 +78,29 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
7178
* whether the <code>ForceAuthn</code> attribute should be set to
7279
* <code>true</code>
7380
* @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 attributeConsumingServiceSelector
86+
* the selector to use to specify the Attribute Consuming Service
87+
* index; if <code>null</code>,
88+
* {@link AttributeConsumingServiceSelector#useDefault()} is used
89+
*/
90+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy,
91+
AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
92+
this(forceAuthn, isPassive, setNameIdPolicy, true, null, attributeConsumingServiceSelector);
93+
}
94+
95+
/**
96+
* Create a set of authentication request input parameters. The
97+
* {@link AttributeConsumingServiceSelector#useDefault()} selector is used to
98+
* select the Attribute Consuming Service.
99+
*
100+
* @param forceAuthn
101+
* whether the <code>ForceAuthn</code> attribute should be set to
102+
* <code>true</code>
103+
* @param isPassive
74104
* whether the <code>IsPassive</code> attribute should be set to
75105
* <code>true</code>
76106
* @param setNameIdPolicy
@@ -103,13 +133,44 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
103133
*/
104134
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate,
105135
String nameIdValueReq) {
136+
this(forceAuthn, isPassive, setNameIdPolicy, allowCreate, nameIdValueReq, 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 allowCreate
151+
* the value to set for the <code>allowCreate</code> attribute of
152+
* <code>NameIDPolicy</code> element; <code>null</code> means it's
153+
* not set at all; only meaningful when
154+
* <code>setNameIdPolicy</code> is <code>true</code>
155+
* @param nameIdValueReq
156+
* the subject that should be authenticated
157+
* @param attributeConsumingServiceSelector
158+
* the selector to use to specify the Attribute Consuming Service
159+
* index; if <code>null</code>,
160+
* {@link AttributeConsumingServiceSelector#useDefault()} is used
161+
*/
162+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate,
163+
String nameIdValueReq, AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
106164
this.forceAuthn = forceAuthn;
107165
this.isPassive = isPassive;
108166
this.setNameIdPolicy = setNameIdPolicy;
109167
this.allowCreate = allowCreate;
110168
this.nameIdValueReq = nameIdValueReq;
169+
this.attributeConsumingServiceSelector = attributeConsumingServiceSelector != null
170+
? attributeConsumingServiceSelector
171+
: AttributeConsumingServiceSelector.useDefault();
111172
}
112-
173+
113174
/**
114175
* Create a set of authentication request input parameters, by copying them from
115176
* another set.
@@ -123,6 +184,7 @@ protected AuthnRequestParams(AuthnRequestParams source) {
123184
this.setNameIdPolicy = source.isSetNameIdPolicy();
124185
this.allowCreate = source.isAllowCreate();
125186
this.nameIdValueReq = source.getNameIdValueReq();
187+
this.attributeConsumingServiceSelector = source.getAttributeConsumingServiceSelector();
126188
}
127189

128190
/**
@@ -163,4 +225,11 @@ public boolean isAllowCreate() {
163225
public String getNameIdValueReq() {
164226
return nameIdValueReq;
165227
}
228+
229+
/**
230+
* @return the selector to use to specify the Attribute Consuming Service index
231+
*/
232+
public AttributeConsumingServiceSelector getAttributeConsumingServiceSelector() {
233+
return attributeConsumingServiceSelector;
234+
}
166235
}

0 commit comments

Comments
 (0)