Luca Leonardo Scorcia
Luca Leonardo Scorcia

Reputation: 475

OpenAM - Getting a Session attribute into an OpenID Connect claim

I'm using OpenAM 13.5 configured to have a SAML circle of trust to federate logins to our applications with third-party IdPs. Some of the SAML assertions received by the third party are mapped as Session level attributes. The SAML part is working fine, but I need to connect to OpenAM an application who can talk OpenID Connect. I created an OpenID Connect service, configured the client accordingly and I can login successfully using the flow "App -> OpenAM UI -> 3rd party IDP -> OpenAM OIDC -> App".

The problem is that I can retrieve only the attributes that are mapped to the data store - the session attributes (e.g. AuthLevel, IDP Name, etc) aren't included in the mapped claims.

I tried to edit the OIDC Claims default script which has a session variable that seems to contain what I need, but unfortunately the session variable is always null.

Is this the correct approach? Why is the session null? Is there something I need to enable in order to read it?

Thanks in advance for your help.

Upvotes: 0

Views: 1363

Answers (2)

Luca Leonardo Scorcia
Luca Leonardo Scorcia

Reputation: 475

As described by Bernhard in the comments, once a request arrives to the /userinfo endpoint OpenAM has no way to reconcile the access token with a living session (and the session could not exist any more as well).

However when accessing the claims inside the ID Token by activating the proprietary AM feature "Always include claims in ID Token" the session object is available and we can poll its properties!

For future readers, this is how I modified the OIDC script:

/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
*/
import com.iplanet.sso.SSOException
import com.sun.identity.idm.IdRepoException
import org.forgerock.oauth2.core.UserInfoClaims

/*
* Defined variables:
* logger - always presents, the "OAuth2Provider" debug logger instance
* claims - always present, default server provided claims
* session - present if the request contains the session cookie, the user's session object
* identity - always present, the identity of the resource owner
* scopes - always present, the requested scopes
* requestedClaims - Map<String, Set<String>>
*                  always present, not empty if the request contains a claims parameter and server has enabled
*                  claims_parameter_supported, map of requested claims to possible values, otherwise empty,
*                  requested claims with no requested values will have a key but no value in the map. A key with
*                  a single value in its Set indicates this is the only value that should be returned.
* Required to return a Map of claims to be added to the id_token claims
*
* Expected return value structure:
* UserInfoClaims {
*    Map<String, Object> values; // The values of the claims for the user information
*    Map<String, List<String>> compositeScopes; // Mapping of scope name to a list of claim names.
* }
*/

// user session not guaranteed to be present
boolean sessionPresent = session != null

def fromSet = { claim, attr ->
    if (attr != null && attr.size() == 1){
        attr.iterator().next()
    } else if (attr != null && attr.size() > 1){
        attr
    } else if (logger.warningEnabled()) {
        logger.warning("OpenAMScopeValidator.getUserInfo(): Got an empty result for claim=$claim");
    }
}

attributeRetriever = { attribute, claim, identity, session, requested ->
    if (requested == null || requested.isEmpty()) {
        fromSet(claim, identity.getAttribute(attribute))
    } else if (requested.size() == 1) {
        requested.iterator().next()
    } else {
        throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
    }
}

sessionAttributeRetriever = { attribute, claim, identity, session, requested ->
    if (requested == null || requested.isEmpty()) {
        if (session != null) {
            fromSet(claim, session.getProperty(attribute))
        } else {
            null
        }
    } else if (requested.size() == 1) {
        requested.iterator().next()
    } else {
        throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
    }
}

// [ {claim}: {attribute retriever}, ... ]
claimAttributes = [
        "email": attributeRetriever.curry("mail"),
        "address": { claim, identity, session, requested -> [ "formatted" : attributeRetriever("postaladdress", claim, identity, session, requested) ] },
        "phone_number": attributeRetriever.curry("telephonenumber"),
        "given_name": attributeRetriever.curry("givenname"),
        "zoneinfo": attributeRetriever.curry("preferredtimezone"),
        "family_name": attributeRetriever.curry("sn"),
        "locale": attributeRetriever.curry("preferredlocale"),
        "name": attributeRetriever.curry("cn"),
        "spid_uid": attributeRetriever.curry("employeeNumber"),
        "spid_idp": attributeRetriever.curry("idpEntityId"),
        "spid_gender": attributeRetriever.curry("description"),
        "spid_authType": sessionAttributeRetriever.curry("AuthType"),
        "spid_authLevel": sessionAttributeRetriever.curry("AuthLevel"),
]

// {scope}: [ {claim}, ... ]
scopeClaimsMap = [
        "email": [ "email" ],
        "address": [ "address" ],
        "phone": [ "phone_number" ],
        "profile": [ "given_name", "zoneinfo", "family_name", "locale", "name" ],
        "spid": [ "spid_uid", "spid_idp", "spid_authType", "spid_authLevel", "spid_gender" ],
]

if (logger.messageEnabled()) {
    scopes.findAll { s -> !("openid".equals(s) || scopeClaimsMap.containsKey(s)) }.each { s ->
        logger.message("OpenAMScopeValidator.getUserInfo()::Message: scope not bound to claims: $s")
    }
}

def computeClaim = { claim, requestedValues ->
    try {
        [ claim, claimAttributes.get(claim)(claim, identity, session, requestedValues) ]
    } catch (IdRepoException e) {
        if (logger.warningEnabled()) {
            logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
        }
    } catch (SSOException e) {
        if (logger.warningEnabled()) {
            logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
        }
    }
}

def computedClaims = scopes.findAll { s -> !"openid".equals(s) && scopeClaimsMap.containsKey(s) }.inject(claims) { map, s ->
    scopeClaims = scopeClaimsMap.get(s)
    map << scopeClaims.findAll { c -> !requestedClaims.containsKey(c) }.collectEntries([:]) { claim -> computeClaim(claim, null) }
}.findAll { map -> map.value != null } << requestedClaims.collectEntries([:]) { claim, requestedValue ->
    computeClaim(claim, requestedValue)
}

def compositeScopes = scopeClaimsMap.findAll { scope ->
    scopes.contains(scope.key)
}

return new UserInfoClaims((Map)computedClaims, (Map)compositeScopes)

I also had to add the java.util.ArrayList$Itr class to the script classes whitelist.

Thanks for your help!

Upvotes: 0

Bernhard Thalmayr
Bernhard Thalmayr

Reputation: 2744

You can not retrieve an SSO session property in OIDC claimscript because the OAuth2 client does not send the SSO tracking cookie in the token request.

It's only possible if you use AM proprietary feature 'always include claims in ID token'.

Upvotes: 1

Related Questions