Add additional claims to the JWT token

, par B. Degoy

The OAuth 2.0 specifications allow the addition of new claims to the payload of the JWT token.
This article shows how this is done within the framework of OAuthSD.

Use case

It should be ensured that the end-user is well able to access protected resources, whether personal data or the application owns the data. We also want to be able to control the data transmitted by a protected resource to an application based on the rights of the end user in this application.

Administrators as well as end users will use OpenID Connect to authenticate to an application. We will want to modulate the rights and privileges of the end user according to his identity, his membership or his profile. The simplest example is the distinction between data read rights and edit rights. One can also consider assigning the user administrative rights to all or part of an application.

This is not the role of the authentication server, which must be transparent with respect to the authorization scopes implemented by an application. For this, OAuthSD allows an external application to incorporate additional declarations to JWT tokens, whether it is a JWT-format access token or an identity token. Protected resources "will know how to" interpret JWT data to control their response.

Note on data security

Recall that the payload of the JWT token is not encrypted, but only "URL64-encoded". Therefore, sensitive data should not be transmitted in this way.

The advantage of passing additional data into the payload of the JWT token is to unmistakably bind them using the signature to the identity of the client and to the user. This is particularly relevant for passing the privileges of a user or application to a protected resource. With this in mind, we can :
– directly transmit the privileges ; for example a role that would take values ​​such as ’administrator’, ’editor’, ’owner’, ’user’, etc., or any other code understood by the resource,
– or transmit the user information that will allow the protected resource to establish its privileges.

But be careful in this second case : it should not reveal sensitive data on the user. If transmission of such data to the protected resource is required, the protected resource can query the Userinfo service.
One could think of an encrypted token (JWE), but it would require, besides a specific development, to deviate from the standard OpenID Connect. It would be much simpler to encrypt the additional information in one block and pass it into a single statement.

Passing end-user privileges with authentication

OAuthSD offers two complementary methods for incorporating information about an end user into the JWT :

– writing additional data in the ’profile’ and ’scope’ fields of the users table using HTTP Rest service. This is an asynchronous process because the data is written at a time chosen by an external application, regardless of the authorization server query.

OAuthSD uses the scope ’privileges’ to control access to this information. When an application presents this scope in the authorization request, the ’profile’ and ’scope’ declarations are entered in the Userinfo response and in the JWT token payload.

– Incorporating additional data refreshed when building the JWT token, for example by querying an external service. It is a synchronous process or "dynamic scope", to obtain information in phase with those held by the user management applications in the case where this management is external to the server.

This second method is developed now.

Incorporation of fresh additional data in JWT payload

This feature is specific to using OAuthSD in a group of applications controlled by the same organization (Corporate Realm).

The oauth2-server-php library provides a reconfigurable function for inserting additional declarations into a JWT at the time it is created.

To incorporate additional data, you must :
– Set the EXTRA_PAYLOAD configuration constant to ’true’.
– define the contents of the private_payload () function of the file /commons/oidc/interface/privatepayload.php.

The private_payload () function must be written by the application designer. It can be a call to a third party web-service. The function receives the parameters client_id, user_id and scope and must return a ’claim’ => ’value’ array which will be combined with the payload declarations without replacing the standard declarations.

Notes :
– This method allows you to create declarations with any name. However, the OIDC standard declarations (’id’, ’jti’, ’iss’, ’aud’, ’sub’, ’exp’, ’iat’, ’token_type’ and ’scope’) can not be overloaded : they will keep their name and their value. For instance, if a ’scope’ declaration is returned, it will be incorporated into the JWT payload as "extra_scope".

– The information is refreshed when the JWT token is created or refreshed. This implies that this information is not strictly synchronous with its corresponding in the external system. If a user’s rights change in the external system, it will be necessary to revoke the user’s session in order to trigger a new authentication request which will result in the creation of a new JWT token. But in the case where it would be necessary to change a large number of users, another way to ensure synchronism will be to close the service over the life of a JWT.

– This method does not write in the users table and, in particular, in the "scope" field. If the additional declarations include a "scope" declaration (which will usually be the case), the value of the "scope" field of the users table will be incorporated into the JWT payload under the name "user_scope".

– In any case, the value of the ’scope’ parameter passed to the authorize call by the application will be embedded under the name ’requested_scope’ [1].

See under the code for an example that retrieves additional data from a Web service.

P.-S.

Example : Additional Data from a Web Service

In OAuthSD, this feature is implemented as follows :

First of all, you need to define the necessary constants in the configuration :

/** [dnc13']
* If true, Authorize will call the function obtain_extra_payload() of 
* /oidc/interface/privatepayload.php to obtain extra data for claims.  
*/
define('EXTRA_PAYLOAD', true);
/* If not null, the function obtain_extra_payload() will check this WS to obtain extra data.  
*/
define('WS_EXTRA_PAYLOAD_URL', 'https://xxxx.xxx');

The extra_payload function is defined as the reconfigurable function called when building the payload of the JWT :

//[dnc13'] Enable getting extra payload for JWT
if ( EXTRA_PAYLOAD ) {
    $server_config['jwt_extra_payload_callable'] = 'extra_payload';
}

The function is defined in the folder /oidc/interface/privatepayload.php. Here is an example obtaining the data by call to a web service :

PHP

<?php
/* privatepayload.php
OauthSD project

Auteur : Bertrand Degoy 
Copyright (c) 2019 DnC  
Licence GPLv3
*/


/** [dnc13'] Projet I
* Function called by Callback function extra_payload itself called by 
* JwtAccessToken::createPayload ()
* Returns declarations to be inserted into the identity token.
* The data is extracted from a webservice.
* Two returns from the webservice are accepted:
* - a scalar value: it will be assigned to the 'extra_scope' declaration in the payload of the identity token.
* - an array in JSON format: after decoding in array, each first level component will be 
* prepared to be incorporated into the payload of the JWT token.
* See :
*   https://oa.dnc.global/web/133
*   https://github.com/bshaffer/oauth2-server-php/pull/804
*   
* @param string $user_id : end user identified by the Authorize controller,
* @param string $client_id : client application passed to Authorize and Token controllers,
* @param string $requested_scopes : the scopes passed to the Authorize controller, 
* @param mixed $cnx : PDO object connector to the database.
* @return array (empty on error).
*/
function private_payload( $client_id, $user_id, $requested_scope ) {

    if ( EXTRA_PAYLOAD ) {

        $extra = obtain_extra_payload( $client_id, $user_id, $requested_scope );

        $result = array();

        if ( $extra !== false ) {

            if ( is_array( $a_extra = json_decode($extra, true)) ) {         
                // array     
                $standards = "id jti iss aud sub exp iat token_type scope";
                foreach( $a_extra as $key => $value ) {
                    if ( strpos($standards, $key) !== false ) {
                        $key = 'extra_' . $key;
                    }
                    $result[$key] = $value;
                }

            } else if ( !empty($extra) ) {
                // scalar value: extra_scope declaration
                $result['extra_scope'] = $extra;

            } else {
                // empty
                // Connect to database
                global $connection;
                $cnx = new \PDO($connection['dsn'], $connection['username'], $connection['password']);
                // Log error
                log_error('ExtraPayloadWS', 'Empty data', $client_id, $user_id, 901, 0, $cnx);

            } 

        } else {
            // Communication error or WS error. Already in the log.
        }

    }

    // integrate requested_scope if $result is not empty
    if ( !empty($result) )
        $result['requested_scope'] = $requested_scope;

    return $result;  //empty in case of error
}


/** [dnc13'] Projet I
* Obtain additional data from a webservice.
* Authorization to access the WS is obtained by passing the secret of the application by the * Authorization Bearer method.
*@param string $user_id : end user identified by the Authorize controller,
* @param string $client_id : client application passed to Authorize and Token controllers,
* @param string $requested_scopes : the scopes passed to the Authorize controller, 
* @return mixed false, string or array() the data received from the WS is returned without modification.
*/
function obtain_extra_payload( $client_id, $user_id, $requested_scope ) {

    if ( ! is_null(WS_EXTRA_PAYLOAD_URL)) {

        include_once(__DIR__ . '../../commons/configure_oidc.php');

        // Connect to database
        global $connection;
        $cnx = new \PDO($connection['dsn'], $connection['username'], $connection['password']); 

        // Get the client secret
        $stmt = $cnx->prepare(sprintf('SELECT client_secret FROM %s WHERE client_id=:client_id', $storage_config['client_table']));
        $stmt->execute(compact('client_id'));
        $data = $stmt->fetch(\PDO::FETCH_ASSOC);

        if ( !empty($client_secret = trim($data['client_secret']) ) ) { 

            // Obtain data from WS at WS_EXTRA_PAYLOAD_URL
            $data = array(  // Parameters are POSTed to the WS.
                'user_id'       => $user_id, 
                'client_id'     => $client_id,
                'passed_scope'  => $requested_scope,  // value passed to Authorize in the request    
            );
            $h = curl_init(WS_EXTRA_PAYLOAD_URL); 
            curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($h, CURLOPT_TIMEOUT, 10);
            curl_setopt($h, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $client_secret));
            curl_setopt($h, CURLOPT_POST, true);
            curl_setopt($h, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));   
            curl_setopt($h, CURLOPT_POSTFIELDS, http_build_query($data));
            //curl_setopt($h, CURLOPT_SSL_VERIFYPEER, false);

            $result = curl_exec($h);

            if ( !empty($result) ) {       
                curl_close($h);
                return $result;

            } else {
                // Curl error during WS request.
                $error = curl_error($h);   
                curl_close($h);
                // Log error
                log_error('ExtraPayload', 'Comm or WS error: ' . $error, $client_id, $user_id, 900, 0, $cnx);
                return false;
            }

        } else
            // No client secret, public application?
            return false;

    } else
        // No WS URL 
        return false;

}

Notes :

– The authentication of the call to the web service is done by passing the secret of the application by the method Authorization : Bearer. This assumes that the WS is integrated with the server’s monitoring application, which accesses the data of the client applications. If this were not the case, it would be necessary to modify the code to pass a secret chain known to both parties.

– The web service receives the identifiers of the client application and the end user, as well as the ranges requested by the application. It can be used to apply business rules that will lead to defining the answer, or not responding.

– Implementation of the extra_payload () function in oauth2-server-php :
see https://github.com/bshaffer/oauth2-server-php/pull/804

Notes

[1] The name "scope" is unfortunately used by Oauth 2.0 for different concepts. It is therefore natural to use different names to avoid mixing or overwriting data.