Getting a Session attribute into an OpenID Connect claim

This topic contains 6 replies, has 2 voices, and was last updated by  l.scorcia 1 week, 5 days ago.

  • Author
    Posts
  • #26173
     l.scorcia 
    Participant

    Hi! 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.

    #26175
     Peter Major 
    Moderator

    Which grant type are you using to obtain the id_token?

    #26177
     l.scorcia 
    Participant

    Hi Peter!
    The code is using the authorization_code grant type.

    This is the GET request the webapp executes to the OpenID authorize endpoint:

    /oauth2/authorize?client_id=openIdTest&redirect_uri=https%3A%2F%2Flocalhost%3A44328%2Fauthorization-code%2Fcallback&response_mode=form_post&response_type=code%20id_token%20token&scope=openid%20profile%20email%20spid%20mise&state=OpenIdConnect.AuthenticationProperties%3DUxUpsLMDeAC3yHBI99-8dR78rtJcjhmq1bk3tQuF-u37grisWi1si3sj2BD5F8f3cq6qvuBUqn72c8J7LRSjPvh16BJDGpbod4qAUf33X4HJrwtrMK6zZkEdQNGYRpgFxu43IeuCFmo-vMhvkPFalEQrkdmDHhyZcwsZ-X28p_O6l_PdTgEzlYrZX8rLrsiqwPxDG1XA8WhjORRvAm49sQ&nonce=637008629573613329.YWVjMTNmZmItYjE5Ny00MWRjLWE3YTYtZjAxYzA0MWQyMTAxZDk1MTJjY2EtOWQ2MC00MmZmLWJmNmUtYmFiMjY0NzZlYjE0&x-client-SKU=ID_NET461&x-client-ver=5.5.0.0 HTTP/1.1
    Host: ***
    Connection: keep-alive
    Cache-Control: max-age=0
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36
    Sec-Fetch-Mode: navigate
    Sec-Fetch-User: ?1
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
    Sec-Fetch-Site: cross-site
    Referer: https://localhost:44328/
    Accept-Encoding: gzip, deflate, br
    Accept-Language: it,en;q=0.9,en-US;q=0.8
    Cookie: i18next=it; amlbcookie=01; iPlanetDirectoryPro=***

    Followed by the POST to get the Access Token:

    /oauth2/access_token HTTP/1.1
    Accept: application/json
    Content-Type: application/x-www-form-urlencoded
    Host: ***
    Content-Length: 194
    Expect: 100-continue

    grant_type=authorization_code&code=bc2f8178-14ce-459c-b54e-c931166ca33c&redirect_uri=https%3A%2F%2Flocalhost%3A44328%2Fauthorization-code%2Fcallback&client_id=openIdTest&client_secret=openIdTest

    And by the GET to retrieve the user info:

    /oauth2/userinfo HTTP/1.1
    Accept: application/json
    Authorization: Bearer e2d96870-1419-4998-a5eb-41be699de4d8
    Host: ***

    Thanks for your help,
    Luca

    • This reply was modified 1 week, 5 days ago by  l.scorcia.
    #26180
     Peter Major 
    Moderator

    Could you clarify if you want the session property inside the issued id_token or if you want to have it in the userinfo response?

    #26181
     l.scorcia 
    Participant

    I would prefer to read the session properties in the UserInfo response – if that’s not possible, it would be ok to retrieve them from the id_token (I would have to activate the “return claims inside the id_token” feature in that case, right?).

    #26183
     Peter Major 
    Moderator

    By the time you access the /userinfo endpoint, the session may not be valid any more, and in the /userinfo request only an OAuth2 access token was provided, which does not have any relation to an underlying session. Putting session properties into id_token should work for authorization code and implicit grant types, it should not work for ROPC grant type though.

    #26185
     l.scorcia 
    Participant

    Hi Peter,
    I had also previously tried to supply the id_token in an http header called iPlanetDirectoryPro when calling the UserInfo endpoint, in order to give the endpoint some information about the session, but it did not have any effect.

    However, your suggestion did the trick! Setting the “Always return claims in ID Tokens” property gives the ability to access the Session object, and I don’t need to call the UserInfo endpoint anymore. 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!
    Luca

Viewing 7 posts - 1 through 7 (of 7 total)

You must be logged in to reply to this topic.

©2019 ForgeRock - we provide an identity and access platform to secure every online relationship for the enterprise market, educational sector and even entire countries. Click to view our privacy policy and terms of use.

Log in with your credentials

Forgot your details?