Skip to content

Commit 1db7ec9

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 820f581 commit 1db7ec9

14 files changed

+1296
-73
lines changed

README.md

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ onelogin.saml2.sp.x509certNew =
247247
# 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
248248
onelogin.saml2.sp.privatekey =
249249

250+
# Attribute Consuming Services
251+
# SEE BELOW
252+
250253
## Identity Provider Data that we want connect with our SP ##
251254

252255
# Identifier of the IdP entity (must be a URI)
@@ -492,9 +495,88 @@ The getSPMetadata will return the metadata signed or not based on the security p
492495

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

495-
##### Attribute Consumer Service(ACS)
496-
This code handles the SAML response that the IdP forwards to the SP through the user's client.
498+
##### Attribute Consuming Service (ACS)
499+
The SP may optionally specify one or more Attribute Consuming Services in its metadata. These can be configured in the settings.
500+
501+
If just one ACS is required:
502+
503+
```properties
504+
# Attribute Consuming Service name when just one ACS should be declared by the SP.
505+
# Comment out or set to empty if no ACS should be declared, or if multiple ones should (see below).
506+
# The service name is mandatory.
507+
onelogin.saml2.sp.attribute_consuming_service.name = My service
508+
509+
# Attribute Consuming Service description when just one ACS should be declared by the SP.
510+
# Ignored if the previous property is commented or empty.
511+
# The service description is optional.
512+
onelogin.saml2.sp.attribute_consuming_service.description = My service description
513+
514+
# Language used for Attribute Consuming Service name and description when just one ACS should be declared by the SP.
515+
# Ignored if the name property is commented or empty.
516+
# The language is optional and default to "en" (English).
517+
onelogin.saml2.sp.attribute_consuming_service.lang = en
518+
519+
# Requested attributes to be included in the Attribute Consuming Service when just one ACS should be declared by the SP.
520+
# At least one requested attribute must be specified, otherwise schema validation will fail.
521+
# Attribute properties are indexed properties, starting from 0. The index is used only to enumerate and sort attributes, but it's required.
522+
# The following properties allow to define each requested attribute:
523+
# - name: mandatory
524+
# - name_format: optional; if omitted, defaults to urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified
525+
# - friendly_name: optional; if omitted, it won't appear in SP metadata
526+
# - required: optional; if omitted or empty, defaults to false
527+
# - value[x]: an attribute value; the [x] is only used only to enumerate and sort values, but it's required
528+
# Please note that only simple values are currently supported and treated internally as strings. Hence no structured values
529+
# and no ability to specify an xsi:type attribute.
530+
# Attribute values are optional and most often they are simply omitted.
531+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name = Email
532+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
533+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].friendly_name = E-mail address
534+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].required = true
535+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[0] = foo@example.org
536+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[1] = bar@example.org
537+
```
538+
539+
If multiple ACSs are required, they can be specified in a similar way, but using indexes: these indexes are used to enumerate and
540+
identify attribute consuming services within the SP metadata and can be subsequently used in the auth process to specify which
541+
attribute set should be requested to the IdP. The "default" property can also be set to designate the default ACS. Here is an example:
542+
543+
```properties
544+
onelogin.saml2.sp.attribute_consuming_service[0].name = Just e-mail
545+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name = Email
546+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
547+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].friendly_name = E-mail address
548+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].required = true
549+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[0] = foo@example.org
550+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[1] = bar@example.org
551+
onelogin.saml2.sp.attribute_consuming_service[1].name = Anagrafica
552+
onelogin.saml2.sp.attribute_consuming_service[1].description = Set completo
553+
onelogin.saml2.sp.attribute_consuming_service[1].lang = it
554+
onelogin.saml2.sp.attribute_consuming_service[1].default = true
555+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[0].name = FirstName
556+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].name = LastName
557+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].required = true
558+
```
559+
560+
Please note that if you specify (multiple) indexed Attribute Consuming Services, the non-indexed properties will be ignored.
561+
562+
As said, to request a specific attribute set when initiating SSO, a selection mechanism is available:
563+
564+
```java
565+
import static com.onelogin.saml2.authn.AttributeConsumingServiceSelector.*;
566+
Auth auth = new Auth(request, response);
567+
// select by index 1
568+
auth.login(new AuthnRequestParams(false, false, true, byIndex(1));
569+
// or select by ACS name
570+
auth.login(new AuthnRequestParams(false, false, true, byServiceName(auth.getSettings(), "Anagrafica"));
571+
// or see AttributeConsumingServiceSelector interface implementations for more options
497572
```
573+
574+
If no selector is specified, `AttributeConsumingServiceSelector.useDefault()` will be used, which will simply omit any
575+
`AttributeConsumingServiceIndex` from the request, hence leaving the IdP choose the default attribute set agreed upon.
576+
577+
Then, the following code handles the SAML response that the IdP forwards to the SP through the user's client:
578+
579+
```java
498580
Auth auth = new Auth(request, response);
499581
auth.processResponse();
500582
if (!auth.isAuthenticated()) {
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
@@ -262,6 +262,12 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
262262
}
263263

264264
valueMap.put("requestedAuthnContextStr", requestedAuthnContextStr);
265+
266+
String attributeConsumingServiceIndexStr = "";
267+
final Integer acsIndex = params.getAttributeConsumingServiceSelector().getAttributeConsumingServiceIndex();
268+
if (acsIndex != null)
269+
attributeConsumingServiceIndexStr = " AttributeConsumingServiceIndex=\"" + acsIndex + "\"";
270+
valueMap.put("attributeConsumingServiceIndexStr", attributeConsumingServiceIndexStr);
265271

266272
return new StrSubstitutor(valueMap);
267273
}
@@ -271,7 +277,7 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
271277
*/
272278
private static StringBuilder getAuthnRequestTemplate() {
273279
StringBuilder template = new StringBuilder();
274-
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}\">");
280+
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}\">");
275281
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
276282
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
277283
return template;

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,15 @@ public class AuthnRequestParams {
2222
*/
2323
private final String nameIdValueReq;
2424

25+
/*
26+
* / Selector to use to specify the Attribute Consuming Service index
27+
*/
28+
private AttributeConsumingServiceSelector attributeConsumingServiceSelector;
29+
2530
/**
26-
* Create a set of authentication request input parameters.
31+
* Create a set of authentication request input parameters. The
32+
* {@link AttributeConsumingServiceSelector#useDefault()} selector is used to
33+
* select the Attribute Consuming Service.
2734
*
2835
* @param forceAuthn
2936
* whether the <code>ForceAuthn</code> attribute should be set to
@@ -35,11 +42,13 @@ public class AuthnRequestParams {
3542
* whether a <code>NameIDPolicy</code> should be set
3643
*/
3744
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy) {
38-
this(forceAuthn, isPassive, setNameIdPolicy, null);
45+
this(forceAuthn, isPassive, setNameIdPolicy, null, null);
3946
}
4047

4148
/**
42-
* Create a set of authentication request input parameters.
49+
* Create a set of authentication request input parameters. The
50+
* {@link AttributeConsumingServiceSelector#useDefault()} selector is used to
51+
* select the Attribute Consuming Service.
4352
*
4453
* @param forceAuthn
4554
* whether the <code>ForceAuthn</code> attribute should be set to
@@ -53,10 +62,57 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
5362
* the subject that should be authenticated
5463
*/
5564
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq) {
65+
this(forceAuthn, isPassive, setNameIdPolicy, nameIdValueReq, null);
66+
}
67+
68+
/**
69+
* Create a set of authentication request input parameters.
70+
*
71+
* @param forceAuthn
72+
* whether the <code>ForceAuthn</code> attribute should be set to
73+
* <code>true</code>
74+
* @param isPassive
75+
* whether the <code>isPassive</code> attribute should be set to
76+
* <code>true</code>
77+
* @param setNameIdPolicy
78+
* whether a <code>NameIDPolicy</code> should be set
79+
* @param attributeConsumingServiceSelector
80+
* the selector to use to specify the Attribute Consuming Service
81+
* index; if <code>null</code>,
82+
* {@link AttributeConsumingServiceSelector#useDefault()} is used
83+
*/
84+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy,
85+
AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
86+
this(forceAuthn, isPassive, setNameIdPolicy, null, attributeConsumingServiceSelector);
87+
}
88+
89+
/**
90+
* Create a set of authentication request input parameters.
91+
*
92+
* @param forceAuthn
93+
* whether the <code>ForceAuthn</code> attribute should be set to
94+
* <code>true</code>
95+
* @param isPassive
96+
* whether the <code>isPassive</code> attribute should be set to
97+
* <code>true</code>
98+
* @param setNameIdPolicy
99+
* whether a <code>NameIDPolicy</code> should be set
100+
* @param nameIdValueReq
101+
* the subject that should be authenticated
102+
* @param attributeConsumingServiceSelector
103+
* the selector to use to specify the Attribute Consuming Service
104+
* index; if <code>null</code>,
105+
* {@link AttributeConsumingServiceSelector#useDefault()} is used
106+
*/
107+
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq,
108+
AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
56109
this.forceAuthn = forceAuthn;
57110
this.isPassive = isPassive;
58111
this.setNameIdPolicy = setNameIdPolicy;
59112
this.nameIdValueReq = nameIdValueReq;
113+
this.attributeConsumingServiceSelector = attributeConsumingServiceSelector != null
114+
? attributeConsumingServiceSelector
115+
: AttributeConsumingServiceSelector.useDefault();
60116
}
61117

62118
/**
@@ -71,6 +127,7 @@ protected AuthnRequestParams(AuthnRequestParams source) {
71127
this.isPassive = source.isPassive();
72128
this.setNameIdPolicy = source.isSetNameIdPolicy();
73129
this.nameIdValueReq = source.getNameIdValueReq();
130+
this.attributeConsumingServiceSelector = source.getAttributeConsumingServiceSelector();
74131
}
75132

76133
/**
@@ -102,4 +159,11 @@ protected boolean isSetNameIdPolicy() {
102159
protected String getNameIdValueReq() {
103160
return nameIdValueReq;
104161
}
162+
163+
/**
164+
* @return the selector to use to specify the Attribute Consuming Service index
165+
*/
166+
public AttributeConsumingServiceSelector getAttributeConsumingServiceSelector() {
167+
return attributeConsumingServiceSelector;
168+
}
105169
}

0 commit comments

Comments
 (0)