OPAttributeResolution
Overview
OIDC deals in "claims" instead of "attributes" but the concept is the same and the AttributeResolverConfiguration and (optionally) AttributeRegistryConfiguration are used as with SAML and CAS to pull in the appropriate data and encode it into JSON in responses.
The only special consideration for OIDC is the specialized encoding requirements, which are handled (as in SAML) either via "inline" AttributeEncoders, or by defining "transcoding" rules that map between IdPAttributes and JSON-encoded claims.
An example file, conf/examples/oidc-attribute-resolver.xml, is included that contains some examples of custom encoders, definitions that match the optional "default" claim rules, and suggested starting points for producing the "sub" claim (see also OPSubClaim).
Timing of Resolution
The Authorize, Token and Userinfo profile endpoints all trigger attribute resolution and filtering phases. The motivation for this is to maintain an implementation that is as stateless as possible to avoid complex clustering technologies. Each endpoint is able to resolve attributes independently and therefore attribute information need not be stored to server side. This also ensures up to date values for claims returned when an access token is refreshed for extended period of time.
(For long time Shibboleth deployers, this is somewhat analagous to the old days when SAML attribute queries were common.)
There are a few obvious downsides to this stateless approach. One is that, depending on the OIDC response type, the same attribute may be resolved more than once. This is particularly an issue if the resolution process is costly and the intention is only to authenticate the user. The ResultCache feature is helpful for this case.
A harder problem is when attribute resolution on the back-channel in the Token and Userinfo endpoints are unable to resolve attributes based on session/authentication/client information, as it is unavailable. This places additional requirements on the resolver configuration to fail gracefully in such cases. As an alternative, there is an encodedAttributes profile configuration property that instructs the Authorize endpoint to encode the attributes needed in later phases into the Authorization Code and/or Access Token directly, making them available to the Token and Userinfo endpoints. This naturally increases the size of the code/token but for specific cases may be tolerable.
Reserved Claims
Because OIDC is string-based, rather than reliant on XML and URIs for uniqueness, there are a number of arbitrary claim names that need to be avoided and will not be produced if used.
Please be aware that OAuth and OpenID are defined unsafely to leverage claim names that are strings rather than URIs. Thus, there are no mechanisms in place to ensure control and uniqueness over the naming of claims as can be done in SAML. As a result, we cannot guarantee that new specifications will not define new claim names and stomp on your use of new claim names that happen to collide.
The only real solution to this problem is simply to avoid “small/simple” names and, when using OIDC at least, avoid adding custom claims to the ID Token and leverage the UserInfo endpoint to produce them instead.
The following claim names are currently reserved (but note the warning above):
aud
iss
iat
exp
acr
auth_time
at_hash
c_hash
nonce
sid
The "sub" claim is also semi-reserved but does come from your configuration. However it has to meet certain requirements and so cannot just contain arbitrary data without risking severe consequences to RPs. It is analagous to violating the expectations of a SAML SP regarding the content of an Attribute, but with more consistently severe problems. Please refer to the dedicated page at OPSubClaim for specifics.
Transcoding Rules
The mappings from traditional LDAP (and SAML) attributes and OIDC claims is much less self-evident, but we have produced a set of plausible default rules that map from typical LDAP attributes to appropriate claims. These rules are not enabled by default, but can be found in conf/attributes/oidc-claim-rules.xml and imported into conf/attributes/default-rules.xml (with or without changes). That file also provides numerous examples to follow in building your own rules if desired.
Note that it's optional and not required to "combine" SAML and OIDC rules together. The system is intelligent enough to allow multiple "outbound" rules associated with the same IdPAttribute id with different transcoders involved.
The additional supported properties and transcoder types are described below. As with SAML, OIDC includes mechanisms to request claims, and so the transcoders support bidirectional mappings to allow decoding of requested JSON claims as well as encoding of IdPAttributes into JSON.
Common Properties
In addition to the generic properties, all OIDC transcoders support the following:
Name | Type | Default | Description |
---|---|---|---|
oidc.name | String |
| The claim name to map to and from (if absent, the IdPAttribute's id is used) |
oidc.asArray | Boolean | false | Encodes and decodes multiple values as a JSON array |
oidc.asInteger | Boolean | false | Encodes and decodes individual values as a JSON integer |
oidc.asBoolean | Boolean | false | Encodes and decodes individual values as a Boolean |
oidc.stringDelimiter | String | <space> | Encodes and decodes multiple values as a string with a specifie delimiter |
oidc.nameFromSamlMetadata4.1 | Boolean | false | The claim name may be set in the SAML metadata |
SAML metadata-driven claim name
Since OP v4.1, the claim name may be set in the SAML metadata-driven style. If the OIDC transcoder property oidc.nameFromSamlMetadata
is set to true
, the metadata tag http://shibboleth.net/ns/attributes/naming/oidc
can be used together with pattern <id> <name>
to set a custom name for the claim.
Below is an example snippet of SAML metadata for test_rp
that configures the claim name eppnNewNameForTesting
for attribute with ID eduPersonPrincipalName
:
...
<md:EntityDescriptor entityID="test_rp">
<md:Extensions>
<mdattr:EntityAttributes>
<saml:Attribute Name="http://shibboleth.net/ns/attributes/naming/oidc"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue>eduPersonPrincipalName eppnNewNameForTesting</saml:AttributeValue>
</saml:Attribute>
...
Transcoder Types
There are 3 built-in types of OIDC transcoders, as follows. Each one is predefined as a Spring bean for use in rules using the "short" name of the transcoder as shown.
OIDCStringTranscoder
The simplest and most commonly used transcoder, it supports encoding and decoding internal values from and to the StringAttributeValue class. It supports the following additional optional property:
Name | Type | Default | Description |
---|---|---|---|
oidc.asObject | Boolean | false | Encodes and decodes a string value as a JSON object (meaning parsing to and from JSON) |
OIDCScopedStringTranscoder
Supports encoding and decoding internal values from and to the ScopedStringAttributeValue class. It supports the following additional properties (all optional):
Name | Type | Default | Description |
---|---|---|---|
oidc.scopeDelimiter | String | @ | The character(s) to use to separate the value and scope |
OIDCByteTranscoder
Supports encoding and decoding internal values from and to the ByteAttributeValue class, with a base64 transform applied. It supports no additional properties.
Attribute Encoders
The alternative to the generality of the transcoding approach is the older style of embedding <AttributeEncoder>
elements within the AttributeResolverConfiguration to specify individual encoding of AttributeDefinitions to claims. This is supported for OIDC via a set of extensions that add new encoder plugin types.
Note for upgraders: the older "token placement" settings that were supported here for splitting claims have been moved to profile configuration settings and are no longer valid here.
Because these are extensions, their xsi:type
definitions live in a separate XML namespace and schema (unfortunately an unavoidable callback to the days when multiple namespaces were required in configurations).
Namespace: urn:mace:shibboleth:2.0:resolver:oidc
Schema: http://shibboleth.net/schema/oidc/shibboleth-attribute-encoder-oidc.xsd
In order to use any of these encoders, the root element of the resolver configuration file needs to be adjusted to declare a namespace prefix for the extension namespace and add in the schema location to address some fundamental XML bugs in Spring:
<AttributeResolver xmlns="urn:mace:shibboleth:2.0:resolver"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oidc="urn:mace:shibboleth:2.0:resolver:oidc"
xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd
urn:mace:shibboleth:2.0:resolver:oidc http://shibboleth.net/schema/oidc/shibboleth-attribute-encoder-oidc.xsd">
All of the extension encoders support the common settings described on the AttributeEncoderPluginConfiguration page. They also support all of these common XML attributes:
Name | Type | Default | Description |
---|---|---|---|
asObject | Boolean | false | Encodes (and decodes) string-valued data as a JSON object instead of a string value |
asBoolean | Boolean | false | Encodes (and decodes) values as booleans (anything but "true", ignoring case, is treated as false) |
asArray | Boolean | false | Encodes (and decodes) multiple values into a JSON array rather than a delimited string |
asInt | Boolean | false | Encodes (and decodes) values as JSON integers (non-numeric values are ignored) |
stringDelimiter | String | <space> | Sets the delimiter to use when encoding multiple values into a single JSON string |
The supported xsi:type
values (and additional options) are:
xsi:type | Description |
---|---|
oidc:OIDCString | Basic encoder for string-valued claims (defaults to encoding multiple values as a space-delimited string) |
oidc:OIDCScopedString | Encoder for scoped claims. A |
oidc:OIDCByte | Encoder for base64-encoding byte-valued data |
Examples
The expected JSON is shown in XML comments. Unrelated aspects are omitted such as dependencies and so forth.
<AttributeDefinition xsi:type="Scoped" id="affiliation" scope="example.org">
<AttributeEncoder xsi:type="oidc:OIDCScopedString" name="eduPersonScopedAffiliaton"/>
</AttributeDefinition>
<!-- eduPersonScopedAffiliaton: "member@example.org student@example.org" -->
<AttributeDefinition xsi:type="Scoped" id="affiliation" scope="example.org">
<AttributeEncoder xsi:type="oidc:OIDCScopedString" name="eduPersonScopedAffiliaton" asArray="true"/>
</AttributeDefinition>
<!-- eduPersonScopedAffiliaton: [ "member@example.org", "student@example.org"] -->
<AttributeDefinition id="address" xsi:type="ScriptedAttribute">
<Script>
<![CDATA[
address.addValue("{\"street_address\":\""+street_address.getValues().get(0) + "\","
+"\"locality\":\""+locality.getValues().get(0) + "\","
+"\"region\":\""+region.getValues().get(0) + "\","
+"\"postal_code\":\""+postal_code.getValues().get(0) + "\","
+"\"country\":\""+country.getValues().get(0) + "\"}");
]]>
</Script>
<AttributeEncoder xsi:type="oidc:OIDCString" asObject="true" name="address" />
</AttributeDefinition>
<!--
"address": {
"street_address":"234 Hollywood Blvd.",
"country":"US",
"locality":"Los Angeles",
"region":"CA",
"postal_code":"90210"
}
-->