Accueil > OpenID Connect OAuth Server par DnC > Développer > OpenID Connect > API Open ID Connect : Introspection

API Open ID Connect : Introspection

L’introspection permet à un serveur de ressource (RS) étranger à l’organisation gérant le serveur d’authentification (AS) de valider un jeton d’identité (ID Token).

Cela permet également de rafraîchir les informations sur le processus d’authentification à l’origine de la création du jeton.

Avantages et inconvénients de l’Introspection

La validation du jeton d’identité (ID Token) auprès du serveur d’authentification (introspection) présente quatre avantages importants :
- cette méthode ne nécessite pas de connaître la clé publique de l’application cliente pour valider la signature du jeton, ce qui permet à des serveurs de ressource étrangers à l’organisation de valider les jetons reçus ;
- elle permet de savoir si le jeton a été révoqué ;
- on peut obtenir des informations sur l’utilisateur final (qui est à l’origine de l’autorisation) qui permettent d’identifier cet utilisateur et, donc, d’agir en fonction de la confiance à accorder d’après son profil ;
- il est possible de mettre en œuvre la déclaration "jti" (JWT ID) qui permet au serveur d’autorisation de vérifier que le jeton n’a pas déjà été utilisé ;
- OAuthSD propose également la vérification de l’IP du demandeur : si une ressource protégée a transmis l’adresse IP de son propre demandeur avec le paramètre ’requester_ip’, on vérifie que cette adresse IP se trouve dans le sous-réseau de l’application cliente identifiée par la déclaration ’aud’. Cela est éeesntiel pour ne pas répondre à un malware ayant intercepté le jeton.

Elle a cependant l’inconvénient d’augmenter le trafic avec le serveur d’autorisation.

Remarque quant au lien entre jeton d’identité et jeton d’accès et l’intérêt réel de ce dernier

Justin Richer : (openid-specs-ab)

"Il n’y a pas de relation 1:1 entre le jeton d’accès et jeton d’identité (ID Token), surtout si vous considérez que le jeton d’accès peut être actualisé ou ne pas expirer, alors que le jeton d’identité doit expirer. Le ID Token représente l’événement authn (et la session, dans une certaine mesure), alors que le jeton d’accès représente une autorisation déléguée d’accès aux informations de profil de l’utilisateur. Vous pouvez obtenir des informations sur l’événement d’authentification qui a généré le jeton d’accès à partir de l’introspection, mais il est important de se rappeler que le jeton d’accès n’est pas destiné à être étroitement lié à cet événement d’authentification. En fait, c’est toute la question d’OAuth qui lie de façon lâche l’authentification au jeton d’accès et maintient le jeton d’accès valide alors que l’utilisateur n’est plus là.

Cette remarque à propos de la faiblesse du lien entre jeton d’accès et jeton d’identité rejoint la conclusion émise à propos de la requête Userinfo : toute requête Userinfo devrait suivre le cycle : demande d’autorisation, validation du ID Token, demande Userinfo, vérification de la concordance des user_id. C’est ce qui est décrit dans les exemples.

Que vaut réellement un jeton d’accès si :
- on ne peut vérifier sa validité sans introspection [1] (autant alors faire l’introspection pour le jeton d’identité),
- on n’est pas certain du lien avec le jeton d’identité ?

Plus généralement, ne devrait-on pas conclure ainsi : plutôt que d’utiliser le jeton d’accès ne vaudrait-il pas mieux utiliser exclusivement le jeton d’identité ? Notons qu’il existe un flux OpenID Connect ne retournant que l’ID Token.

Implémentation de l’Introspection

Il n’y a pas (à ce jour) de "norme" définissant l’Introspection pour OpenID Connect. Cependant, OAuthSD, ainsi que les implémentations courantes, se fonde sur la proposition de standard RFC 7662 : OAuth 2.0 Token Introspection. DnC a proposé une fonction d’introspection pour la bibliothèque OAuth 2.0 PHP.

Proposition de standard RFC 7662

Traduction d’un extrait du document RFC 7662 : OAuth 2.0 Token Introspection

2.2. Réponse d’introspection

Le serveur répond avec un objet JSON [RFC7159] dans le format "application/json "avec les membres de niveau supérieur suivants.

active
CHAMPS OBLIGATOIRE. booléen indiquant si le jeton présenté est actuellement actif ou non. Les spécificités de l’état "actif" d’un jeton variera en fonction de la mise en œuvre du serveur d’autorisation et les informations qu’il conserve sur ses jetons, mais une "vraie"
valeur retournée pour la propriété "active" indiquera généralement qu’un jeton donné a été émis par ce serveur d’autorisation, n’a pas été révoqué par le propriétaire de la ressource et relève de sa fenêtre de validité donnée (par exemple, après son heure d’émission et avant son heure d’expiration). Voir la section 4 pour des informations sur la mise en œuvre de ces contrôles.

scope
OPTIONNEL. Une chaîne JSON contenant une liste de portées associées à ce jeton, dans le format décrit dans la Section 3.3 de OAuth 2.0 [RFC6749].

client_id
OPTIONNEL. Identifiant de client pour le client OAuth 2.0 qui a demandé ce jeton.

username
OPTIONNEL. Identifiant lisible par l’homme pour le propriétaire de la ressource qui a autorisé ce jeton.

token_type
OPTIONNEL. Type de jeton tel que défini dans la section 5.1 de OAuth 2.0 [RFC6749].

exp
OPTIONNEL. Horodatage entier, mesuré en nombre de secondes depuis le 1er janvier 1970 UTC, en indiquant la date d’expiration de ce jeton, comme défini dans JWT [RFC7519].

iat
OPTIONNEL. Horodatage entier, mesuré en nombre de secondes depuis le 1er janvier 1970 UTC, en indiquant quand ce jeton a été publié à l’origine, tel que défini dans JWT [RFC7519].

nbf
OPTIONNEL. Horodatage entier, mesuré en nombre de secondes depuis le 1er janvier 1970 UTC, en indiquant quand ce jeton ne doit pas être utilisé auparavant, comme défini dans JWT [RFC7519].

sub
OPTIONNEL. Sujet du jeton, tel que défini dans JWT [RFC7519].
Généralement, un identifiant lisible par machine du propriétaire de la ressource qui a autorisé ce jeton.

aud
OPTIONNEL. chaîne Identifiant spécifique au service ou liste de chaînes identifiants représentant le public visé pour ce jeton, comme défini dans JWT [RFC7519].

iss
OPTIONNEL. Chaîne représentant l’émetteur de ce jeton, sous la forme définie dans JWT [RFC7519].

jti
OPTIONNEL. Identificateur de chaîne pour le jeton, tel que défini dans JWT [RFC7519].

Des implémentations spécifiques PEUVENT étendre cette structure avec leurs propres noms de réponse spécifiques aux services en tant que membres de niveau supérieur de cet objet JSON. Les noms de réponse destinés à être utilisés sur plusieurs domaines DOIVENT être inscrit dans le registre "OAuth Token Introspection Response" défini à la section 3.1.

Le serveur d’autorisation PEUT répondre différemment à différentes ressources protégées faisant la même demande. Par exemple, un serveur d’autorisation PEUT limiter les portées d’un jeton donné retourné pour chaque ressource protégée pour empêcher une ressource protégée d’en apprendre davantage sur le réseau que nécessaire pour son fonctionnement.

La réponse PEUT être mise en cache par la ressource protégée pour améliorer les performances et réduire la charge sur le point final d’introspection, mais au prix de la qualité de la validité des informations utilisées par la ressource protégée pour prendre des décisions d’autorisation. Voir la section 4 pour plus d’informations en ce qui concerne le compromis lorsque la réponse est mise en cache.

Forme de la demande d’Introspection

Point d’extrémité d’introspection (Introspection Endpoint)

https://oa.dnc.global/introspect

Le jeton est passé avec le paramètre "token" par l’une des méthodes suivantes : Auth Header ou POST (GET est possible mais non sécurisé).

Une seule méthode doit être utilisée à la fois, sinon le serveur retourne ’400, invalid_request’.

Méthode Auth Header :
C’est la méthode que nous recommandons.

Méthodes POST :
Lorsque vous placez le jeton dans le corps de la requête HTTP, la méthode doit être POST ou PUT. Sinon, le serveur retourne 400, ’invalid_request’, ’When putting the token in the body, the method must be POST or PUT’, ’#section-2.2’.

Le type de contenu pour les requêtes POST doit être "application / x-www-form-urlencoded. Si ce n’est pas le cas, le serveur retourne 400, ’invalid_request’, ’The content type for POST requests must be "application/x-www-form-urlencoded".

Méthodes GET :
Cette méthode n’est pas recommandée pour des raisons de sécurité.

Réponse du serveur

En cas de succès, le serveur retourne une réponse HTTP 200.

Le corps de la réponse contient un tableau portant les informations suivantes :

index type valeur
status entier code HTTP
headers string Headers de la réponse
page string JSON Array :
active : true,
scope : (JSON string Array) scopes associés au jeton.
client_id : ID de l’application cliente qui a demandé ce jeton.
username : ID OAuth de l’utilisateur final (human-readable).
exp : (long) (secondes depuis le 1° janvier 1970). Unix Time de la fin de validité du jeton.
iat : (long) (secondes depuis le 1° janvier 1970). Unix Time de création du jeton
iss : (string) issuer : serveur d’authentification qui a diffusé ce jeton.
sub : identifiant interne de l’utilisateur qui a autorisé ce jeton.
aud : audience définie pour ce jeton.

 

Si le jeton n’est pas valide, alors que la requête n’a pas échoué, l’introspection ne retourne un code HTTP 200 et une réponse active : false.

Note :
- Le traitement des erreurs d’introspection décrit dans ce qui suit est propre à cette version de OAuthSD, destinée au développement et à la mise au point d’une application mettant en oeuvre la délégation d’authentification. Conformément à la spécification, les serveurs OAuthSD de production ne donnent pas de détail sur l’erreur, mais retournent simplement un code HTTP 200 et une réponse active : false.

En cas d’échec de la requête, le corps de la réponse contient :

index type valeur
page string JSON Array :
error : titre de l’erreur,
error_description : description de l’erreur

La réponse HTTP ainsi que les valeurs de error et error_description sont données par le tableau suivant :

Réponse error
titre de l’erreur
error_description
description de l’erreur
Explication
400 invalid_request Only one method may be used to authenticate at a time (Auth header, GET or POST) La requête est mal formée
400 invalid_request Missing parameters : "token" is required La requête Introspection requiert le paramètre ’token’.
400 invalid_request When putting the token in the body, the method must be POST or PUT Si on place le token dans le corps de la requête, la méthode ne peut être que POST ou PUT
400 invalid_request The content type for POST requests must be "application/x-www-form-urlencoded l’IETF spécifie ce type de contenu. NB : tous les serveurs Web ne remplissent pas cette variable _SERVER voir http://tools.ietf.org/html/rfc6750#section-2.2
401 invalid_token JWT is malformed le jeton JWT ne peut être décodé.

Exemples

Demande de validation d’un jeton d’identité, méthode Auth Header (ou "JWT Bearer" ) :

PHP

  1. /*
  2. Autorisation avec OAuth Server by DnC
  3. OpenID Connect : Introspection, méthode Auth Header
  4. */
  5. function oauth_authorize($idtoken) {
  6.  
  7.     $Ok = false;
  8.  
  9.     if ( !empty( $idtoken) ) {
  10.  
  11.         $h = curl_init(AUTHENTICATION_SERVER_URL . 'introspect');
  12.         curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
  13.         curl_setopt($h, CURLOPT_TIMEOUT, 10);
  14.         curl_setopt($h, CURLOPT_HTTPHEADER,
  15.             array('Authorization: Bearer ' . $idtoken));
  16.        
  17.         $response = curl_exec($h);
  18.      
  19.         if ( (int)curl_getinfo($h)['http_code'] === 200 ) {  
  20.             $jwt = json_decode($response, true);  
  21.             $Ok = ( $jwt['active'] == true );
  22.         }
  23.     }
  24.  
  25.     if ( $Ok AND isset($_SERVER["HTTP_REFERER"]) ) {
  26.         $urlParts = parse_url($_SERVER["HTTP_REFERER"]);
  27.         if ( $urlParts['host'] !== $_SERVER["HTTP_HOST"] ) {
  28.             // CORS : autoriser l'origine
  29.             $issuer = $urlParts['scheme'] . "://" . $urlParts['host'];
  30.             include_spip('inc/headers');    
  31.             header('Access-Control-Allow-Origin', $issuer);
  32.         }
  33.     }
  34.  
  35.     return $Ok;
  36.  
  37. }

Télécharger

Variante avec méthode GET pour SPIP :
SPIP

  1. /*
  2. Autorisation avec OAuth Server by DnC
  3. OpenID Connect : Introspection, méthode GET  
  4. */
  5.  
  6. function oauth_authorize($idtoken) {
  7.  
  8.     $Ok = false;
  9.  
  10.     if ( !empty( $idtoken) ) {
  11.  
  12.         // Interroger l'introspection de OAuth Server by DnC
  13.         include_spip('inc/distant');
  14.         $url = "http://oa.dnc.global/introspect?token=" . $idtoken;
  15.         $response = recuperer_url($url);  
  16.        
  17.         if ( (int)$response['status'] === 200 ) {
  18.             $jwt = json_decode($response['page'], true);
  19.             if ( $jwt['active'] == true ) {
  20.                 if ( isset($_SERVER["HTTP_ORIGIN"]) ) {
  21.                     // Accès HTTP (CORS) : autoriser l'origine
  22.                     include_spip('inc/headers');
  23.                     $issuer = trim(strtr($_SERVER["HTTP_ORIGIN"], '<>"\'', '[]##'));    
  24.                     header('Access-Control-Allow-Origin', $issuer);
  25.                 }
  26.                 $Ok = true;
  27.             }
  28.         }
  29.  
  30.     }
  31.  
  32.     return $Ok;
  33.  
  34. }

Télécharger

Un exemple plus complet, faisant apparaître la totalité des erreurs possibles, figure ici : OpenID Connect : Exemples complets du flux d’Autorisation via un code puis requête UserInfo.

Notes :

- Dans le cas où l’application cliente et le serveur de données protégées se trouvent dans des domaines différents, il faut gérer l’autorisation HTTP, comme cela est fait dans l’exemple ci-dessus. Voyez Contrôle d’accès HTTP (CORS).

- L’interrogation du serveur d’autorisation à chaque accès d’une ressource protégée peut le surcharger. Pour éviter cela, on peut mettre en cache la réponse du serveur du côté du serveur de ressource. Avec SPIP, c’est le rôle de la fonction recuperer_url_cache() qui pourra remplacer recuperer_url() dans l’exemple précédent. La fonction permet de régler le délai de garde en cache, qu’il convient de fixer à une durée assez courte (10 secondes par exemple), l’essentiel étant de ne pas bombarder le serveur. Voici un exemple :

SPIP

  1. $res = recuperer_url_cache( $url, array('delai_cache' => 10) ); // Délai de 10s

La fonction décrite précédemment peut être utilisée dans une fonction d’autorisation d’accès à un objet SPIP ( ici l’objet gis de radar ) :

SPIP

  1. function _autoriser_gis($faire, $quoi, $id, $qui, $options) {
  2.     if ( $qui['statut'] == '0minirezo' ) {
  3.         // Toujours autoriser un administrateur
  4.         return true;
  5.     } else {
  6.         if ( $idtoken = $_GET['token'] ) {
  7.             // Vérifier le jeton d'accès
  8.             return oauth_authorize($idtoken);    
  9.         } else return false;
  10.     }
  11. }

Télécharger

Il faut cependant noter que la mise en cache expose à la réutilisation du jeton par un malware.

Paramètre ’requester_ip’

Lorsque l’Introspection est demandée par une ressource protégée (distincte de l’application cliente à l’origine de l’authentification), il importe de ne pas répondre à un malware ayant intercepté le jeton et tentant de le ré-utiliser.

Pour cela, la ressource protégée doit transmettre l’IP du demandeur au moyen du paramètre ’requester_ip’.

La fonction d’introspection d’OAuthSD vérifie que l’IP indiquée se trouve dans le sous réseau de l’application cliente.

Exemple d’appel, avec le jeton passé par la méthode Auth Header et le paramètre ’requester_ip’ par Post :
PHP

  1.             // Method Bearer + parameters by Post
  2.             $data = array(
  3.                 'requester_ip' => $_SERVER['SERVER_ADDR'],
  4.             );
  5.             $authorization = "Authorization: Bearer ". $res1['id_token'];
  6.             $h = curl_init($introspection_endpoint);
  7.             curl_setopt($h, CURLOPT_HTTPHEADER, array(
  8.                 'Content-Type: application/x-www-form-urlencoded',
  9.                 'Authorization: Bearer ' . $id_token
  10.             ));
  11.             curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
  12.             curl_setopt($h, CURLOPT_POST, 1);  
  13.             curl_setopt($h, CURLOPT_POSTFIELDS, http_build_query($data));
  14.             ...

Télécharger