OpenID Connect : Exemples complets du flux d’Autorisation via un code puis requête UserInfo

, par B. Degoy

Les scripts testopenid testent le flux d’Autorisation via un code (Authorization Code Flow), et enchaînent sur une demande UserInfo, en ne laissant passer aucune erreur : tous les cas non conformes répertoriés dans cette documentation donneront lieu à l’affichage d’un message d’erreur.

Les deux méthodes de validation du JWT sont illustrées : Validation locale, Introspection.

Ces scripts ne sont pas conçu pour une application en production. Cependant, ils fournissent un flux complet que vous pourrez adapter au besoin de votre application cliente. En particulier, il serait opportun d’utiliser les fonctions d’affichage et de gestion des erreurs du système de l’application cliente.

Notez que ces scripts jouent le rôle de Point d’extrémité de redirection, ou URI de retour à l’application cliente (Redirection Endpoint). Voyez : OAuth 2.0 : Points d’extrémité.

 

Première version : Validation locale du JWT

La vérification du jeton JWT est effectuée localement par la fonction decode_jwt(), qui est décrite dans cet article : JSON Web Token (JWT).

PHP

<?php
/*
testopenid3.php

Verbose Test of OpenID Connect Authorization Code Flow

This code is meant for testing, not for production !

At the authentification step, if you don't want to create an account 
on OAuthSD, use this credentials : 
login = bebert 
password = 012345678

Author : 
Bertrand Degoy https://degoy.com
Credits :
bschaffer https://github.com/bshaffer/oauth2-server-php

Licence : MIT licence
Copyright (c) 2016 - DnC

*/

//***** Configuration *****

/* Client's Public Key 
Warning : no double-quote, no indent! 
*/
define ('PUBLIC_KEY', 
'-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2dCHybdu/1pDk5BHSxMA
nMGygm8lm6s2hnru3GPH2JyMxrd92dC7TljI21P+egsnmjsUzaS1IWZPvIFvEOwO
5wP8gFyNm8fFkXSmrAEHXLFJ6WufF+f5Fg3pU5GDPjT5Z7ccWab5NM9w7ec433J1
XtePh2QjUbibu4rpwYh8ADODAJkyIRMhhYXqK0n8GgojkcgEZ172sB/NdbcNALPy
Qg0lMd3/AKxavTSSm9LslEyP+ZwBvTENzhoeQV2V7ZQ5xIZs6VBgrnsnYbgfbdQ+
Dbk2FRbtB2+g4rKbN04JheaZyFORseoigVJ6asQ5lUS/3cMIUj2C+VBj4xAyXp00
TMH8GtiGIQkVYjBd/Lsza11YwBOA8YYvDnTs/kzy9CqHjETdIHUlNqeaFbHSYTST
rUl9QxBN+JAVGs9YY9MWoiVsGex4MsTwf3PanKIlavKXeFSwppwMMvmdt+yrGraH
UKv5QP5NMbOu6/BghbuQZP4MoUnRxQxt8PN2e5M2b358C3tctgRQhRGBWaYw8B5J
/drz5VA8s14NkG162lBW7PLYhLqm8u2hpqIlOCVndwW2W+bCkXrfjj3jBHe4yauQ
vyQWcv3KaBV2HsUoY2sCAaC5nB46SV0UkAycX8xyqOsGJA64m2S+ntOQkB9R2x2y
4DjfJHTRTe2uXsaiYFahoLECAwEAAQ==
-----END PUBLIC KEY-----
');

$client_id = 'testopenid';
$client_secret = 'thesecret';

$authorization_endpoint = 'https://oa.dnc.global/authorize';
$token_endpoint = 'https://oa.dnc.global/token';
$userinfo_endpoint = 'https://oa.dnc.global/userinfo';

//*** End of configuration ***

ini_set('display_errors', 1);

// Set session
session_save_path('/home/oadnc/sessions_oauthsd');          
session_name('oauthsd'); 
session_start();   


if ( !isset($_GET['error']) ) {

    if ( isset($_GET['code']) ) {

        if ( isset($_GET['state']) ) {

            // Check state
            if ( $_GET['state'] == $_SESSION['state'] ) {
            
                // Step 2. Token request

                $code = $_GET['code'];

                $data = array(
                    'grant_type' => 'authorization_code',
                    'code' => $code,
                );
                $h = curl_init($token_endpoint);
                curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($h, CURLOPT_TIMEOUT, 10);
                curl_setopt($h, CURLOPT_USERPWD, "{$client_id}:{$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);

                $res = curl_exec($h);

                if ($res)  {

                    curl_close($h);
                    $res = json_decode($res, true);

                    if  ( empty($res['error'] ) ) {

                        // Validate signed JWT token using client's public key
                        if ( $payload = decode_jwt($res['id_token'], 
                                                        PUBLIC_KEY, 'RS256') ) {

                            // If Token Response is valid goto step 3
                            // Step 3. Get UserInfo
                            $access_token = $res['access_token'];

                            /* Auth Header Methode
                            $headr = array();
                            $headr[] = 'Authorization: Bearer ' . $access_token;
                            $h = curl_init();
                            curl_setopt($h, CURLOPT_URL, $userinfo_endpoint); 
                            curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
                            curl_setopt($h, CURLOPT_TIMEOUT, 10);
                            curl_setopt($h, CURLOPT_HTTPHEADER, $headr);
                            //curl_setopt($h, CURLOPT_SSL_VERIFYPEER, false);
                            //*/

                            //* Post Methode  
                            $data2 = array(
                                'access_token' => $access_token,
                                'state' => $state,
                            );
                            $h = curl_init($userinfo_endpoint);
                            curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
                            curl_setopt($h, CURLOPT_TIMEOUT, 10);
                            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($data2));
                            //*/

                            $res = curl_exec($h);

                            if ( $res ) {

                                curl_close($h);
                                $res = json_decode($res, true);

                                if  ( empty($res['error'] ) ) {

                                    // Check User ID
                                    if ( $payload['sub'] == $res['sub'] ) {

                                        // Everithing Ok !
                                        echo "UserInfo Response:\n";
                                        print_r($res);

                                    } else  
                                        // User of ID Token doesn't match UserInfo's one
                                        exit('User mismatch, got : ' . $res['sub']);

                                } else
                                    // Token request error
                                    exit ('UserInfo Request error : ' . $res['error'] . ' : ' 
                                                     . $res['error_description']);

                            } else {
                                // Curl error during UserInfo request
                                exit('UserInfo Request error : ' . curl_error($h));
                                curl_close($h);
                            } 

                        } else
                            // Invalid id_token 
                            exit('Error : Invalid ID Token');

                    } else {
                        // Token request error
                        exit ('Token request error : ' . $res['error'] . ' : ' 
                                              . $res['error_description']);
                    }

                } else {
                    // Curl error during Token request
                    exit('Token Request error : ' . curl_error($h));
                    curl_close($h);
                }
            
            } else 
                // Wrong State
                exit("Authorization error : incoherent State");

        } else 
            // Missing State
            exit("Authorization error : missing State");
            
    } else {

        // Step 1. Authorization Code request

        @session_regenerate_id();
        $state = session_id();
        $_SESSION['state'] = $state;
        
        $data = array(
            'response_type' => 'code',
            'client_id' => $client_id,
            'scope' => 'openid profile',
            'state' => $state,    
        );

        $authorization_endpoint .= '?' . http_build_query($data);
        header('Location: ' . $authorization_endpoint);
        exit();
    }

} else {
    // Authorization error 
    exit("Authorization error : {$_GET['error']} : {$_GET['error_description']}");
}

Notons que, dans cet exemple, nous avons intégré la clé publique dans le code. Une alternative consiste à se la procurer auprès d’AuthSD, voyez : API OpenId Connect : Point d’extrémité d’informations sur les clefs (Keys Endpoint). Mais la meilleure pratique est de valider le jeton par introspection.

Lancer le script : https://oa.dnc.global/oidc/tests/te...

Lors de la demande de vos login et mot de passe, si vous ne voulez pas vous inscrire sur ce serveur en tant qu’utilisateur, vous pourrez utiliser les identifiants suivants :
E-mail or pseudo : bebert
Password : 012345678

Voici la page de login générée par le serveur OAuthSD :

 

A propos du scope sli : vous noterez que le contrôleur Authorize est appelé avec le scope sli, alors que ce n’est pas le cas dans l’exemple suivant. Cela permet d’illustrer le fonctionnement de la Connexion unique (Single Login In, SLI).

<clear1>

<clear1>

Deuxième version : Validation du JWT par Introspection

La vérification du jeton JWT est effectuée par la méthode décrite dans cet article : API Open ID Connect : Introspection (Introspection Endpoint).

PHP

<?php
/*
testopenid4.php

Verbose Test of OpenID Connect JWT Introspection

This code is meant for testing, not for production !

For the authentification step, if you don't want to create an account 
on OAuthSD, use this credentials : 
login = bebert 
password = 012345678

Author : 
Bertrand Degoy https://degoy.com
Credits :
bschaffer https://github.com/bshaffer/oauth2-server-php

Licence : MIT licence
Copyright (c) 2016 - DnC

*/

$client_id = 'testopenid4';
$client_secret = 'thesecret';

$authorization_endpoint = 'https://oa.dnc.global/authorize';
$token_endpoint = 'https://oa.dnc.global/token';
$introspection_endpoint = 'https://oa.dnc.global/introspect'; 
$userinfo_endpoint = 'https://oa.dnc.global/userinfo';

//*** End of configuration ***

ini_set('display_errors', 1);

// Set session
session_save_path('/home/oadnc/sessions_oauthsd');          
session_name('oauthsd'); 
session_start();  


if ( !isset($_GET['error']) ) {

    if ( isset($_GET['code']) ) {

        if ( isset($_GET['state']) ) {

            // Check state
            if ( $_GET['state'] == $_SESSION['state'] ) {

                // Step 2. Token request

                $code = $_GET['code'];

                $data = array(
                    'grant_type' => 'authorization_code',
                    'code' => $code,
                );
                $h = curl_init($token_endpoint);
                curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($h, CURLOPT_TIMEOUT, 10);
                curl_setopt($h, CURLOPT_USERPWD, "{$client_id}:{$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);

                $res = curl_exec($h);

                if ( is_array(json_decode($res, true) ) ) {

                    curl_close($h);
                    $res = json_decode($res, true);
                    
                    $access_token = $res['access_token'];

                    if  ( empty($res['error'] ) ) {

                        // Validate signed JWT token using introspection
                        //* Post Methode  
                        $data1 = array(
                            'token' => $res['id_token'],
                        );
                        $h = curl_init($introspection_endpoint);
                        curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
                        curl_setopt($h, CURLOPT_TIMEOUT, 10);
                        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($data1));
                        //*/

                        $res = curl_exec($h);

                        if ( is_array(json_decode($res, true) ) ) {

                            curl_close($h);
                            $jwt = json_decode($res, true);

                            if  ( empty($jwt['error'] ) ) {

                                if ( $jwt['active'] == 'true' ) {

                                    // If Token Response is valid goto step 3
                                    // Step 3. Get UserInfo

                                    /* Auth Header Methode
                                    $headr = array();
                                    $headr[] = 'Authorization: Bearer ' . $access_token;
                                    $h = curl_init();
                                    curl_setopt($h, CURLOPT_URL, $userinfo_endpoint); 
                                    curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
                                    curl_setopt($h, CURLOPT_TIMEOUT, 10);
                                    curl_setopt($h, CURLOPT_HTTPHEADER, $headr);
                                    //curl_setopt($h, CURLOPT_SSL_VERIFYPEER, false);
                                    //*/

                                    //* Post Methode  
                                    $data2 = array(
                                        'access_token' => $access_token,
                                    );
                                    $h = curl_init($userinfo_endpoint);
                                    curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
                                    curl_setopt($h, CURLOPT_TIMEOUT, 10);
                                    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($data2));
                                    //*/

                                    $res = curl_exec($h);

                                    if ( is_array(json_decode($res, true) ) ) {

                                        curl_close($h);
                                        $res = json_decode($res, true);

                                        if  ( empty($res['error'] ) ) {

                                            // Check User ID
                                            if ( $jwt['sub'] == $res['sub'] ) {

                                                // Everithing Ok !
                                                echo "UserInfo Response:\n";
                                                print_r($res);

                                            } else  
                                                // User of ID Token doesn't match UserInfo's one
                                                exit('User mismatch, got : ' . $res['sub']);

                                        } else
                                            // Token request error
                                            exit ('UserInfo Request error : ' . $res['error'] . ' : '
                                                            . $res['error_description']);

                                    } else { 
                                        if ( !empty($res) ) {
                                            // script error ?
                                            exit ('UserInfo script error : ' . $res); 
                                        } else {
                                            // Curl error during UserInfo request
                                            $error = curl_error($h);
                                            curl_close($h);
                                            exit ('UserInfo request Curl error : ' . $error );
                                        }
                                    } 

                                } else
                                    // JWT is inactive
                                    exit('Error : Invactive ID Token'); 

                            } else
                                // Invalid id_token 
                                exit('Error : Invalid ID Token');

                        } else { 
                            if ( !empty($res) ) {
                                // script error ?
                                exit ('Introspection script error : ' . $res); 
                            } else {
                                // Curl error during Introspection request
                                $error = curl_error($h);
                                curl_close($h);
                                exit ('Introspection request Curl error : ' . $error );
                            }
                        }

                    } else {
                        // Token request error
                        exit ('Token request error : ' . $res['error'] . ' : ' 
                                         . $res['error_description']);
                    }

                } else { 
                    if ( !empty($res) ) {
                        // script error ?
                        exit ('Token script error : ' . $res); 
                    } else {
                        // Curl error during Token request
                        $error = curl_error($h);
                        curl_close($h);
                        exit ('Token request Curl error : ' . $error );
                    }
                }

            } else 
                // Wrong State
                exit("Authorization error : incoherent State");

        } else 
            // Missing State
            exit("Authorization error : missing State");

    } else {

        // Step 1. Authorization Code request

        @session_regenerate_id();
        $state = session_id();
        $_SESSION['state'] = $state;

        $data = array(
            'response_type' => 'code',
            'client_id' => $client_id,
            'scope' => 'openid profile sli',
            'state' => $state,    
        );

        $authorization_endpoint .= '?' . http_build_query($data);
        header('Location: ' . $authorization_endpoint);
        exit();
    }

} else {
    // Authorization error 
    exit("Authorization error : {$_GET['error']} : {$_GET['error_description']}");
}


?>

Lancer le script : https://oa.dnc.global/oidc/tests/te...