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.
DnC s’est inspiré de cette proposition de standard pour étendre les jetons acceptés aux trois types Access Token, Identity Token (JWT) ou Json Web Encryption (JWE).
Point d’extrémité d’introspection
Point d’extrémité d’introspection (Introspection Endpoint)
https://oa.dnc.global/introspect
Forme de la demande d’Introspection
La demande ne doit être effectuée que par la méthode POST.
Note :
OAuthSD ne nécessite pas l’enregistrement d’un scope réservé pour autoriser le client à utiliser l’introspection, contrairement à d’autres implémentations. Le scope ’openid’ est également inutile, le controleur fonctionnant aussi bien dans le cadre de OAuth 2.0 que celui d’OpenID Connect.
Contrôle de l’accès
Les demandes adressées au point de terminaison d’introspection doivent être authentifiées avec les informations d’identification du client (Client Credentials Grant) ou autorisées avec un jeton d’accès au porteur (Bearer Token).
En conséquence, l’application appelante (ou le serveur de ressource) doit être enregistrée comme cliente sur le serveur d’authentification
Client Credentials Grant
C’est l’approche la plus simple et celle qui est recommandée.
L’application appelante (ou le serveur de ressource) doit être enregistrée comme cliente sur le serveur d’authentification [1].
L’authentification est effectuée en utilisant l’authentification HTTP Basic (cf. section 2.3.1 de OAuth 2.0 [RFC6749]). Les identifiants client_id et client_secret sont ceux qui ont été définis lors de l’inscription de l’application cliente sur le serveur.
Bearer Token
Cette approche nécessite un jeton d’accès pour autoriser la demande d’introspection.
Pour un serveur de ressource, cela est plus compliqué du fait de la durée limitée de validité du jeton d’accès, contraignant à une nouvelle demande de jeton. Une façon d’obtenir un tel jeton consiste à inscrire l’application pour le flux Client Credential Grant.
L’authentification est effectuée en passant le jeton dans l’en-tête Authorization de la demande d’introspection.
Note :
OAuthSD permet de sauter cette étape en réglant la constante de configuration AUTHENTICATE_INTROSPECT_REQUEST à false.
De fait, la rfc indique que l’objectif de cette authentification client est "Pour empêcher les attaques par balayage de jetons ..."
Les attaques par balayage (scanning) pourraient être mieux atténuées de certaines autres manières, en particulier au niveau du réseau.
De plus, donner à un client inconnu des informations sur la validité du jeton n’est pas un problème de sécurité élevé.
Requête
Les paramètre suivants doivent être postés :
token (OBLIGATOIRE) : le jeton à valider. Les jetons soumis peuvent être du type Access Token, Identity Token (JWT) ou Json Web Encryption (JWE).
Si un jeton JWE est reconnu, il est déchiffré et le processus se poursuit avec la charge utile du JWE, qui n’est autre que le JWT.
requester_ip (OPTIONNEL) : 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 est celle qui a été enregistré avec l’application cliente ou, à défaut, se trouve dans le sous réseau de l’application cliente tel qu’il peut être déterminé à partir de l’URL de retour.
Notes :
OAuthSD ne nécessite pas le paramètre token_type.
Avertissement à propos du paramètre ’alg’ : la RFC 7515, section 4.1.1 prévoit d’appliquer la valeur du paramètre ’alg’ pour le choix de l’algorithme de validation de la signature.
C’est une faille de sécurité sévère, et donc une erreur de la spécification. L’introspection d’OAuthSD applique la méthode définie pour chaque application, avec laquelle les jetons sont signés, quelle que soit la valeur de ’alg’ et génère une erreur si la valeur est différente.
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 401 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
/*
Autorisation avec OAuth Server by DnC
OpenID Connect : Introspection, méthode Auth Header
*/
function oauth_authorize($idtoken) {
$Ok = false;
if ( !empty( $idtoken) ) {
$h = curl_init(AUTHENTICATION_SERVER_URL
. 'introspect');
array('Authorization: Bearer ' . $idtoken));
$Ok = ( $jwt['active'] == true );
}
}
if ( $Ok AND
isset($_SERVER["HTTP_REFERER"]) ) {
$urlParts = parse_url($_SERVER["HTTP_REFERER"]);
if ( $urlParts['host'] !== $_SERVER["HTTP_HOST"] ) {
// CORS : autoriser l'origine
$issuer = $urlParts['scheme'] . "://" . $urlParts['host'];
include_spip('inc/headers');
header('Access-Control-Allow-Origin', $issuer);
}
}
return $Ok;
}
Télécharger
Variante avec méthode GET pour SPIP :
SPIP
/*
Autorisation avec OAuth Server by DnC
OpenID Connect : Introspection, méthode GET
*/
function oauth_authorize($idtoken) {
$Ok = false;
if ( !empty( $idtoken) ) {
// Interroger l'introspection de OAuth Server by DnC
include_spip('inc/distant');
$url = "http://oa.dnc.global/introspect?token=" . $idtoken;
$response = recuperer_url($url);
if ( (int)$response['status'] === 200 ) {
if ( $jwt['active'] == true ) {
if ( isset($_SERVER["HTTP_ORIGIN"]) ) {
// Accès HTTP (CORS) : autoriser l'origine
include_spip('inc/headers');
$issuer = trim(strtr($_SERVER["HTTP_ORIGIN"], '<>"\'', '[]##'));
header('Access-Control-Allow-Origin', $issuer);
}
$Ok = true;
}
}
}
return $Ok;
}
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
$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
function _autoriser_gis($faire, $quoi, $id, $qui, $options) {
if ( $qui['statut'] == '0minirezo' ) {
// Toujours autoriser un administrateur
return true;
} else {
if ( $idtoken = $_GET['token'] ) {
// Vérifier le jeton d'accès
return oauth_authorize($idtoken);
} else return false;
}
}
Télécharger
Il faut cependant noter que la mise en cache expose à la réutilisation du jeton par un malware.
Exemple d’appel, avec le jeton passé par la méthode Auth Header et le paramètre ’requester_ip’ par Post :
PHP
// Method Bearer + parameters by Post
'requester_ip' => $_SERVER['SERVER_ADDR'],
);
$authorization = "Authorization: Bearer ". $res1['id_token'];
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Bearer ' . $id_token
));
...
Télécharger
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 :
elle permet de savoir si le jeton a été révoqué, contrairement à la validation locale ; c’est là un avantage fondamental pour la sécurité : si vous décidez de révoquer un jeton d’accès, alors il faut bien que les applications réagissent, le seul moyen est l’introspection.
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 ;
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 essentiel pour ne pas répondre à un malware ayant intercepté le jeton.
Elle a pour seul 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 [2] (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é ? C’est pour cela qu’il existe un flux OpenID Connect ne retournant que l’ID Token.