Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple Attribute Consuming Services #307

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ onelogin.saml2.contacts.technical.email_address = [email protected]
onelogin.saml2.contacts.support.given_name = Support Guy
onelogin.saml2.contacts.support.email_address = [email protected]

# Attribute Consuming Services
# SEE BELOW

## Identity Provider Data that we want connect with our SP ##

# Identifier of the IdP entity (must be a URI)
Expand Down Expand Up @@ -510,8 +513,90 @@ The getSPMetadata will return the metadata signed or not based on the security p

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

##### Attribute Consumer Service(ACS)
This code handles the SAML response that the IdP forwards to the SP through the user's client.
##### Attribute Consuming Services
The SP may optionally specify one or more Attribute Consuming Services in its metadata. These can be configured in the settings.

If just one Attribute Consuming Service is required:

```properties
# Attribute Consuming Service name when just one such service should be declared by the SP.
# Comment out or set to empty if no Attribute Consuming Service should be declared, or if multiple ones should (see below).
# The service name is mandatory.
onelogin.saml2.sp.attribute_consuming_service.name = My service

# Attribute Consuming Service description when just one such service should be declared by the SP.
# Ignored if the previous property is commented or empty.
# The service description is optional.
onelogin.saml2.sp.attribute_consuming_service.description = My service description

# Language used for Attribute Consuming Service name and description when just one such service should be declared by the SP.
# Ignored if the name property is commented or empty.
# The language is optional and defaults to "en" (English).
onelogin.saml2.sp.attribute_consuming_service.lang = en

# Requested attributes to be included in the Attribute Consuming Service when just one such service should be declared by the SP.
# At least one requested attribute must be specified, otherwise schema validation will fail.
# Attribute properties are indexed properties, starting from 0. The index is used only to enumerate and sort attributes, but it's required.
# The following properties allow to define each requested attribute:
# - name: mandatory
# - name_format: optional; if omitted, defaults to urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified
# - friendly_name: optional; if omitted, it won't appear in SP metadata
# - required: optional; if omitted or empty, defaults to false
# - value[x]: an attribute value; the [x] index is used only to enumerate and sort values, but it's required
# Please note that only simple values are currently supported and treated internally as strings. Hence no structured values
# and no ability to specify an xsi:type attribute.
# Attribute values are optional and most often they are simply omitted.
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name = Email
onelogin.saml2.sp.attribute_consuming_service.attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
onelogin.saml2.sp.attribute_consuming_service.attribute[0].friendly_name = E-mail address
onelogin.saml2.sp.attribute_consuming_service.attribute[0].required = true
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[0] = [email protected]
onelogin.saml2.sp.attribute_consuming_service.attribute[0].value[1] = [email protected]
```

If multiple Attribute Consuming Services are required, they can be specified in a similar way, but using indexes: these indexes
are used to enumerate and identify attribute consuming services within the SP metadata and can be subsequently used in the auth
process to specify which attribute set should be requested to the IdP. The "default" property can also be set to designate the
default Attribute Consuming Service. Here is an example:

```properties
onelogin.saml2.sp.attribute_consuming_service[0].name = Just e-mail
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name = Email
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].name_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].friendly_name = E-mail address
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].required = true
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[0] = [email protected]
onelogin.saml2.sp.attribute_consuming_service[0].attribute[0].value[1] = [email protected]
onelogin.saml2.sp.attribute_consuming_service[1].name = Anagrafica
onelogin.saml2.sp.attribute_consuming_service[1].description = Set completo
onelogin.saml2.sp.attribute_consuming_service[1].lang = it
onelogin.saml2.sp.attribute_consuming_service[1].default = true
onelogin.saml2.sp.attribute_consuming_service[1].attribute[0].name = FirstName
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].name = LastName
onelogin.saml2.sp.attribute_consuming_service[1].attribute[1].required = true
```

Please note that if you specify (multiple) indexed Attribute Consuming Services, the non-indexed properties will be ignored.

As said, to request a specific attribute set when initiating SSO, a selection mechanism is available:

```java
import static com.onelogin.saml2.authn.AttributeConsumingServiceSelector.*;
Auth auth = new Auth(request, response);
// select by index 1
auth.login(new AuthnRequestParams(false, false, true, byIndex(1));
// or select by service name
auth.login(new AuthnRequestParams(false, false, true, byServiceName(auth.getSettings(), "Anagrafica"));
// or see AttributeConsumingServiceSelector interface implementations for more options
```

If no selector is specified, `AttributeConsumingServiceSelector.useDefault()` will be used, which will simply omit any
`AttributeConsumingServiceIndex` from the request, hence leaving the IdP choose the default attribute set agreed upon.


##### Assertion Consumer Service (ACS)
This code handles the SAML response that the IdP forwards to the SP through the user's client:

```java
Auth auth = new Auth(request, response);
auth.processResponse();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.onelogin.saml2.authn;

import java.util.List;

import com.onelogin.saml2.model.AttributeConsumingService;
import com.onelogin.saml2.settings.Saml2Settings;

/**
* Interfaced used to select the Attribute Consuming Service to be specified in
* an authentication request. An instance of this interface can be passed as an
* input parameter in a {@link AuthnRequestParams} to be used when initiating a
* login operation.
* <p>
* A set of predefined implementations are provided: they should cover the most
* common cases.
*/
@FunctionalInterface
public interface AttributeConsumingServiceSelector {

/**
* @return a selector of the default Attribute Consuming Service
*/
static AttributeConsumingServiceSelector useDefault() {
return () -> null;
}

/**
* @param attributeConsumingService
* the Attribute Consuming Service to select
* @return a selector the chooses the specified Attribute Consuming Service;
* indeed, its index is used
*/
static AttributeConsumingServiceSelector use(final AttributeConsumingService attributeConsumingService) {
return byIndex(attributeConsumingService.getIndex());
}

/**
* @param index
* the index of the Attribute Consuming Service to select
* @return a selector that chooses the Attribute Consuming Service with the
* given index
*/
static AttributeConsumingServiceSelector byIndex(final int index) {
return () -> index;
}

/**
* @param settings
* the SAML settings, containing the list of the available
* Attribute Consuming Services (see
* {@link Saml2Settings#getSpAttributeConsumingServices()})
* @param serviceName
* the name of the Attribute Consuming Service to select
* @return a selector that chooses the Attribute Consuming Service with the
* given name; please note that this selector will select the default
* service if no one is found with the given name
*/
static AttributeConsumingServiceSelector byServiceName(final Saml2Settings settings, final String serviceName) {
return () -> {
final List<AttributeConsumingService> services = settings.getSpAttributeConsumingServices();
if (services != null)
return services.stream().filter(service -> service.getServiceName().equals(serviceName))
.findFirst().map(AttributeConsumingService::getIndex).orElse(null);
else
return null;
};
}

/**
* Returns the index of the selected Attribute Consuming Service.
*
* @return the service index, or <code>null</code> if the default one should be selected
*/
Integer getAttributeConsumingServiceIndex();
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
}

valueMap.put("requestedAuthnContextStr", requestedAuthnContextStr);

String attributeConsumingServiceIndexStr = "";
final Integer acsIndex = params.getAttributeConsumingServiceSelector().getAttributeConsumingServiceIndex();
if (acsIndex != null)
attributeConsumingServiceIndexStr = " AttributeConsumingServiceIndex=\"" + acsIndex + "\"";
valueMap.put("attributeConsumingServiceIndexStr", attributeConsumingServiceIndexStr);

return new StrSubstitutor(valueMap);
}
Expand All @@ -275,7 +281,7 @@ private StrSubstitutor generateSubstitutor(AuthnRequestParams params, Saml2Setti
*/
private static StringBuilder getAuthnRequestTemplate() {
StringBuilder template = new StringBuilder();
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}\">");
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}\">");
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
return template;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,15 @@ public class AuthnRequestParams {
*/
private final String nameIdValueReq;

/*
* / Selector to use to specify the Attribute Consuming Service index
*/
private AttributeConsumingServiceSelector attributeConsumingServiceSelector;

/**
* Create a set of authentication request input parameters.
* Create a set of authentication request input parameters. The
* {@link AttributeConsumingServiceSelector#useDefault()} selector is used to
* select the Attribute Consuming Service.
*
* @param forceAuthn
* whether the <code>ForceAuthn</code> attribute should be set to
Expand Down Expand Up @@ -71,6 +78,29 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
* whether the <code>ForceAuthn</code> attribute should be set to
* <code>true</code>
* @param isPassive
* whether the <code>isPassive</code> attribute should be set to
* <code>true</code>
* @param setNameIdPolicy
* whether a <code>NameIDPolicy</code> should be set
* @param attributeConsumingServiceSelector
* the selector to use to specify the Attribute Consuming Service
* index; if <code>null</code>,
* {@link AttributeConsumingServiceSelector#useDefault()} is used
*/
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy,
AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
this(forceAuthn, isPassive, setNameIdPolicy, true, null, attributeConsumingServiceSelector);
}

/**
* Create a set of authentication request input parameters. The
* {@link AttributeConsumingServiceSelector#useDefault()} selector is used to
* select the Attribute Consuming Service.
*
* @param forceAuthn
* whether the <code>ForceAuthn</code> attribute should be set to
* <code>true</code>
* @param isPassive
* whether the <code>IsPassive</code> attribute should be set to
* <code>true</code>
* @param setNameIdPolicy
Expand Down Expand Up @@ -103,13 +133,44 @@ public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setName
*/
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate,
String nameIdValueReq) {
this(forceAuthn, isPassive, setNameIdPolicy, allowCreate, nameIdValueReq, null);
}

/**
* Create a set of authentication request input parameters.
*
* @param forceAuthn
* whether the <code>ForceAuthn</code> attribute should be set to
* <code>true</code>
* @param isPassive
* whether the <code>isPassive</code> attribute should be set to
* <code>true</code>
* @param setNameIdPolicy
* whether a <code>NameIDPolicy</code> should be set
* @param allowCreate
* the value to set for the <code>allowCreate</code> attribute of
* <code>NameIDPolicy</code> element; <code>null</code> means it's
* not set at all; only meaningful when
* <code>setNameIdPolicy</code> is <code>true</code>
* @param nameIdValueReq
* the subject that should be authenticated
* @param attributeConsumingServiceSelector
* the selector to use to specify the Attribute Consuming Service
* index; if <code>null</code>,
* {@link AttributeConsumingServiceSelector#useDefault()} is used
*/
public AuthnRequestParams(boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, boolean allowCreate,
String nameIdValueReq, AttributeConsumingServiceSelector attributeConsumingServiceSelector) {
this.forceAuthn = forceAuthn;
this.isPassive = isPassive;
this.setNameIdPolicy = setNameIdPolicy;
this.allowCreate = allowCreate;
this.nameIdValueReq = nameIdValueReq;
this.attributeConsumingServiceSelector = attributeConsumingServiceSelector != null
? attributeConsumingServiceSelector
: AttributeConsumingServiceSelector.useDefault();
}

/**
* Create a set of authentication request input parameters, by copying them from
* another set.
Expand All @@ -123,6 +184,7 @@ protected AuthnRequestParams(AuthnRequestParams source) {
this.setNameIdPolicy = source.isSetNameIdPolicy();
this.allowCreate = source.isAllowCreate();
this.nameIdValueReq = source.getNameIdValueReq();
this.attributeConsumingServiceSelector = source.getAttributeConsumingServiceSelector();
}

/**
Expand Down Expand Up @@ -163,4 +225,11 @@ public boolean isAllowCreate() {
public String getNameIdValueReq() {
return nameIdValueReq;
}

/**
* @return the selector to use to specify the Attribute Consuming Service index
*/
public AttributeConsumingServiceSelector getAttributeConsumingServiceSelector() {
return attributeConsumingServiceSelector;
}
}
Loading