Skip to content

Commit 0905b85

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 523786b commit 0905b85

File tree

15 files changed

+1530
-217
lines changed

15 files changed

+1530
-217
lines changed

README.md

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

243+
# Attribute Consuming Services
244+
# SEE BELOW
245+
243246
## Identity Provider Data that we want connect with our SP ##
244247

245248
# Identifier of the IdP entity (must be a URI)
@@ -476,9 +479,88 @@ The getSPMetadata will return the metadata signed or not based on the security p
476479

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

479-
##### Attribute Consumer Service(ACS)
480-
This code handles the SAML response that the IdP forwards to the SP through the user's client.
482+
##### Attribute Consuming Service (ACS)
483+
The SP may optionally specify one or more Attribute Consuming Services in its metadata. These can be configured in the settings.
484+
485+
If just one ACS is required:
486+
487+
```properties
488+
# Attribute Consuming Service name when just one ACS should be declared by the SP.
489+
# Comment out or set to empty if no ACS should be declared, or if multiple ones should (see below).
490+
# The service name is mandatory.
491+
onelogin.saml2.sp.attribute_consuming_service.name = My service
492+
493+
# Attribute Consuming Service description when just one ACS should be declared by the SP.
494+
# Ignored if the previous property is commented or empty.
495+
# The service description is optional.
496+
onelogin.saml2.sp.attribute_consuming_service.description = My service description
497+
498+
# Language used for Attribute Consuming Service name and description when just one ACS should be declared by the SP.
499+
# Ignored if the name property is commented or empty.
500+
# The language is optional and default to "en" (English).
501+
onelogin.saml2.sp.attribute_consuming_service.lang = en
502+
503+
# Requested attributes to be included in the Attribute Consuming Service when just one ACS should be declared by the SP.
504+
# At least one requested attribute must be specified, otherwise schema validation will fail.
505+
# Attribute properties are indexed properties, starting from 0. The index is used only to enumerate and sort attributes, but it's required.
506+
# The following properties allow to define each requested attribute:
507+
# - name: mandatory
508+
# - name_format: optional; if omitted, defaults to urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified
509+
# - friendly_name: optional; if omitted, it won't appear in SP metadata
510+
# - required: optional; if omitted or empty, defaults to false
511+
# - value[x]: an attribute value; the [x] is only used only to enumerate and sort values, but it's required
512+
# Please note that only simple values are currently supported and treated internally as strings. Hence no structured values
513+
# and no ability to specify an xsi:type attribute.
514+
# Attribute values are optional and most often they are simply omitted.
515+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name = Email
516+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
517+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].friendly_name = E-mail address
518+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].required = true
519+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[0] = foo@example.org
520+
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[1] = bar@example.org
521+
```
522+
523+
If multiple ACSs are required, they can be specified in a similar way, but using indexes: these indexes are used to enumerate and
524+
identify attribute consuming services within the SP metadata and can be subsequently used in the auth process to specify which
525+
attribute set should be requested to the IdP. The "default" property can also be set to designate the default ACS. Here is an example:
526+
527+
```properties
528+
onelogin.saml2.sp.attribute_consuming_service[0].name = Just e-mail
529+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name = Email
530+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
531+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].friendly_name = E-mail address
532+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].required = true
533+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[0] = foo@example.org
534+
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[1] = bar@example.org
535+
onelogin.saml2.sp.attribute_consuming_service[1].name = Anagrafica
536+
onelogin.saml2.sp.attribute_consuming_service[1].description = Set completo
537+
onelogin.saml2.sp.attribute_consuming_service[1].lang = it
538+
onelogin.saml2.sp.attribute_consuming_service[1].default = true
539+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[0].name = FirstName
540+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].name = LastName
541+
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].required = true
542+
```
543+
544+
Please note that if you specify (multiple) indexed Attribute Consuming Services, the non-indexed properties will be ignored.
545+
546+
As said, to request a specific attribute set when initiating SSO, a selection mechanism is available:
547+
548+
```java
549+
import static com.onelogin.saml2.authn.AttributeConsumingServiceSelector.*;
550+
Auth auth = new Auth(request, response);
551+
// select by index 1
552+
auth.login(new AuthnRequestParams(false, false, true, byIndex(1));
553+
// or select by ACS name
554+
auth.login(new AuthnRequestParams(false, false, true, byServiceName(auth.getSettings(), "Anagrafica"));
555+
// or see AttributeConsumingServiceSelector interface implementations for more options
481556
```
557+
558+
If no selector is specified, `AttributeConsumingServiceSelector.useDefault()` will be used, which will simply omit any
559+
`AttributeConsumingServiceIndex` from the request, hence leaving the IdP choose the default attribute set agreed upon.
560+
561+
Then, the following code handles the SAML response that the IdP forwards to the SP through the user's client:
562+
563+
```java
482564
Auth auth = new Auth(request, response);
483565
auth.processResponse();
484566
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: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*
2222
* A class that implements SAML 2 Authentication Request
2323
*/
24-
public class AuthnRequest {
24+
public class AuthnRequest extends AuthnRequestParams {
2525
/**
2626
* Private property to construct a logger for this class.
2727
*/
@@ -42,26 +42,6 @@ public class AuthnRequest {
4242
*/
4343
private final Saml2Settings settings;
4444

45-
/**
46-
* When true the AuthNRequest will set the ForceAuthn='true'
47-
*/
48-
private final boolean forceAuthn;
49-
50-
/**
51-
* When true the AuthNRequest will set the IsPassive='true'
52-
*/
53-
private final boolean isPassive;
54-
55-
/**
56-
* When true the AuthNReuqest will set a nameIdPolicy
57-
*/
58-
private final boolean setNameIdPolicy;
59-
60-
/**
61-
* Indicates to the IdP the subject that should be authenticated
62-
*/
63-
private final String nameIdValueReq;
64-
6545
/**
6646
* Time stamp that indicates when the AuthNRequest was created
6747
*/
@@ -81,46 +61,64 @@ public AuthnRequest(Saml2Settings settings) {
8161
* Constructs the AuthnRequest object.
8262
*
8363
* @param settings
84-
* OneLogin_Saml2_Settings
64+
* OneLogin_Saml2_Settings
8565
* @param forceAuthn
86-
* When true the AuthNReuqest will set the ForceAuthn='true'
66+
* When true the AuthNReuqest will set the ForceAuthn='true'
8767
* @param isPassive
88-
* When true the AuthNReuqest will set the IsPassive='true'
68+
* When true the AuthNReuqest will set the IsPassive='true'
8969
* @param setNameIdPolicy
90-
* When true the AuthNReuqest will set a nameIdPolicy
70+
* When true the AuthNReuqest will set a nameIdPolicy
9171
* @param nameIdValueReq
92-
* Indicates to the IdP the subject that should be authenticated
72+
* Indicates to the IdP the subject that should be authenticated
73+
* @deprecated use {@link #AuthnRequest(Saml2Settings, AuthnRequestParams)} with
74+
* {@link AuthnRequestParams#AuthnRequestParams(boolean, boolean, boolean, String)}
75+
* instead
9376
*/
77+
@Deprecated
9478
public AuthnRequest(Saml2Settings settings, boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq) {
95-
this.id = Util.generateUniqueID(settings.getUniqueIDPrefix());
96-
issueInstant = Calendar.getInstance();
97-
this.isPassive = isPassive;
98-
this.settings = settings;
99-
this.forceAuthn = forceAuthn;
100-
this.setNameIdPolicy = setNameIdPolicy;
101-
this.nameIdValueReq = nameIdValueReq;
102-
103-
StrSubstitutor substitutor = generateSubstitutor(settings);
104-
authnRequestString = substitutor.replace(getAuthnRequestTemplate());
105-
LOGGER.debug("AuthNRequest --> " + authnRequestString);
79+
this(settings, new AuthnRequestParams(forceAuthn, isPassive, setNameIdPolicy, nameIdValueReq));
10680
}
107-
81+
10882
/**
10983
* Constructs the AuthnRequest object.
11084
*
11185
* @param settings
112-
* OneLogin_Saml2_Settings
86+
* OneLogin_Saml2_Settings
11387
* @param forceAuthn
114-
* When true the AuthNReuqest will set the ForceAuthn='true'
88+
* When true the AuthNReuqest will set the ForceAuthn='true'
11589
* @param isPassive
116-
* When true the AuthNReuqest will set the IsPassive='true'
90+
* When true the AuthNReuqest will set the IsPassive='true'
11791
* @param setNameIdPolicy
118-
* When true the AuthNReuqest will set a nameIdPolicy
92+
* When true the AuthNReuqest will set a nameIdPolicy
93+
* @deprecated use {@link #AuthnRequest(Saml2Settings, AuthnRequestParams)} with
94+
* {@link AuthnRequestParams#AuthnRequestParams(boolean, boolean, boolean)}
95+
* instead
11996
*/
97+
@Deprecated
12098
public AuthnRequest(Saml2Settings settings, boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy) {
12199
this(settings, forceAuthn, isPassive, setNameIdPolicy, null);
122100
}
123101

102+
/**
103+
* Constructs the AuthnRequest object.
104+
*
105+
* @param settings
106+
* OneLogin_Saml2_Settings
107+
* @param params
108+
* a set of authentication request input parameters that shape the
109+
* request to create
110+
*/
111+
public AuthnRequest(Saml2Settings settings, AuthnRequestParams params) {
112+
super(params);
113+
this.id = Util.generateUniqueID(settings.getUniqueIDPrefix());
114+
issueInstant = Calendar.getInstance();
115+
this.settings = settings;
116+
117+
StrSubstitutor substitutor = generateSubstitutor(settings);
118+
authnRequestString = substitutor.replace(getAuthnRequestTemplate());
119+
LOGGER.debug("AuthNRequest --> " + authnRequestString);
120+
}
121+
124122
/**
125123
* @return the base64 encoded unsigned AuthnRequest (deflated or not)
126124
*
@@ -171,12 +169,12 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
171169
Map<String, String> valueMap = new HashMap<String, String>();
172170

173171
String forceAuthnStr = "";
174-
if (forceAuthn) {
172+
if (isForceAuthn()) {
175173
forceAuthnStr = " ForceAuthn=\"true\"";
176174
}
177175

178176
String isPassiveStr = "";
179-
if (isPassive) {
177+
if (isPassive()) {
180178
isPassiveStr = " IsPassive=\"true\"";
181179
}
182180

@@ -191,6 +189,7 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
191189
valueMap.put("destinationStr", destinationStr);
192190

193191
String subjectStr = "";
192+
String nameIdValueReq = getNameIdValueReq();
194193
if (nameIdValueReq != null && !nameIdValueReq.isEmpty()) {
195194
String nameIDFormat = settings.getSpNameIDFormat();
196195
subjectStr = "<saml:Subject>";
@@ -201,7 +200,7 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
201200
valueMap.put("subjectStr", subjectStr);
202201

203202
String nameIDPolicyStr = "";
204-
if (setNameIdPolicy) {
203+
if (isSetNameIdPolicy()) {
205204
String nameIDPolicyFormat = settings.getSpNameIDFormat();
206205
if (settings.getWantNameIdEncrypted()) {
207206
nameIDPolicyFormat = Constants.NAMEID_ENCRYPTED;
@@ -239,6 +238,12 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
239238
}
240239

241240
valueMap.put("requestedAuthnContextStr", requestedAuthnContextStr);
241+
242+
String attributeConsumingServiceIndexStr = "";
243+
final Integer acsIndex = getAttributeConsumingServiceSelector().getAttributeConsumingServiceIndex();
244+
if (acsIndex != null)
245+
attributeConsumingServiceIndexStr = " AttributeConsumingServiceIndex=\"" + acsIndex + "\"";
246+
valueMap.put("attributeConsumingServiceIndexStr", attributeConsumingServiceIndexStr);
242247

243248
return new StrSubstitutor(valueMap);
244249
}
@@ -248,7 +253,7 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
248253
*/
249254
private static StringBuilder getAuthnRequestTemplate() {
250255
StringBuilder template = new StringBuilder();
251-
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}\">");
256+
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}\">");
252257
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
253258
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
254259
return template;

0 commit comments

Comments
 (0)