Passer au contenu principal
Le mécanisme prompt=login peut être contourné en supprimant simplement le paramètre pendant son passage par l’agent utilisateur (navigateur) et ne sert qu’à fournir une indication d’expérience utilisateur au fournisseur (OP) dans les cas où la (RP) veut afficher un lien comme : « Salut Josh. Ce n’est pas vous ? Cliquez ici. » Toutefois, vous ne devez pas vous y fier pour valider qu’une authentification récente a bien eu lieu. Pour atténuer ce risque, l’application doit valider qu’une réauthentification a eu lieu à l’aide de la revendication auth_time. Cette revendication est automatiquement incluse dans l’ lorsque les paramètres prompt=login ou max_age=0 sont fournis dans la requête d’authentification. Vous devez transmettre le paramètre max_age au point de terminaison /authorize de l’API Authorization. Si vous utilisez Auth0.js ou Lock, vous pouvez définir ce paramètre dans les options appropriées de la bibliothèque. La façon dont vous implémentez la réauthentification dépend de votre cas d’utilisation précis. Faites la distinction entre une simple réauthentification pour des opérations sensibles et l’authentification renforcée (c.-à-d. l’) pour des opérations sensibles. Les deux sont des mesures de sécurité valides. La première oblige l’utilisateur final à saisir de nouveau son mot de passe, tandis que la seconde exige aussi l’utilisation d’un moyen d’authentification multifacteur préconfiguré.

Limites des paramètres prompt=login

La spécification OIDC définit le paramètre prompt=login, qui peut être utilisé pour déclencher une interface de réauthentification (généralement un écran de connexion) :
promptFACULTATIF : liste de valeurs de chaîne ASCII sensibles à la casse, séparées par des espaces, qui précise si le serveur d’autorisation invite l’utilisateur final à se réauthentifier et à donner son consentement. Les valeurs définies sont :loginLe serveur d’autorisation doit inviter l’utilisateur final à se réauthentifier. S’il ne peut pas réauthentifier l’utilisateur final, il doit renvoyer une erreur, généralement login_required.
Toutefois, l’utilisation de ce paramètre pour garantir une réauthentification pose un problème : le RP n’a aucun moyen de valider qu’une réauthentification a bien eu lieu. Examinons le trafic pour comprendre pourquoi. Le flux d’une requête d’authentification provenant du RP est le suivant :
https://mydomain.auth0.com/authorize?
client_id=abcd1234
&redirect_uri= https://mydomain.com/callback
&scope=openid profile
&response_type=id_token
&prompt=login
Après une authentification réussie auprès de l’AS, un jeton d’identité sera remis au RP :
JSON
{
  "nickname": "user",
  "name": "user@mydomain.auth0.com",
  "updated_at": "2019-04-01T14:43:03.445Z",
  "iss": "https://jcain0.auth0.com/",
  "sub": "auth0|l33t",
  "aud": "abcd1234",
  "iat": 1554129793,
  "exp": 1554165793
}
Le document d’identité fiable renvoyé par l’AS ne contient aucune revendication permettant de valider le moment de la dernière connexion. Cela devient problématique lorsque la demande d’autorisation initiale prend la forme d’une redirection 302 via le navigateur de l’utilisateur final. Si un acteur malveillant veut contourner l’étape de réauthentification demandée par le RP, il lui suffit de supprimer le paramètre prompt=login, et le RP ne voit aucune différence dans les champs contenus dans le jeton d’identité. Voici un schéma d’un flux implicite simplifié utilisant le paramètre prompt=login :
Forcer la réauthentification dans un flux implicite OIDC
Notez que l’utilisateur final n’a qu’à supprimer le paramètre prompt=login pour pouvoir ignorer l’étape de réauthentification :
Flux implicite simplifié avec suppression de prompt=login
Le ou les jetons renvoyés par le premier flux ci-dessus seront identiques au ou aux jetons renvoyés par le second flux. Le RP n’a aucun moyen, selon la spécification, de vérifier qu’une réauthentification a bien eu lieu et, par conséquent, ne peut pas se fier au fait qu’un prompt=login ait réellement entraîné une réauthentification.

Paramètre de requête d’authentification max_age

Contrairement à prompt=login, le paramètre de requête d’authentification max_age fournit un mécanisme permettant aux RP de confirmer avec certitude qu’une réauthentification a eu lieu dans un intervalle donné. La spécification OIDC indique :
max_ageFACULTATIF : âge maximal de l’authentification. Spécifie le délai maximal autorisé, en secondes, depuis la dernière authentification active de l’utilisateur final par l’OP. Si le temps écoulé dépasse cette valeur, l’OP doit tenter de réauthentifier activement l’utilisateur final. (Le paramètre de requête max_age correspond au paramètre de requête PAPE max_auth_age d’OpenID 2.0.) Lorsque max_age est utilisé, le jeton d’identité renvoyé doit inclure une revendication auth_time.
La dernière phrase de la définition est la plus importante. Lorsque max_age est demandé par le RP, une revendication auth_time doit être transmise au RP. Cela signifie que max_age peut être utilisé de l’une des deux façons suivantes :
  • Pour imposer une fraîcheur minimale de session : si une application exige que les utilisateurs se réauthentifient une fois par jour, cela peut être imposé dans le contexte d’une session beaucoup plus longue en fournissant une valeur à max_age. Cette valeur est exprimée en secondes.
  • Pour forcer une réauthentification immédiate : si une application exige qu’un utilisateur se réauthentifie avant de lui accorder l’accès, fournissez une valeur de 0 pour le paramètre max_age et l’AS forcera une nouvelle authentification.
Cette exigence est décrite comme suit :
Flux OIDC de réauthentification max_age
Notez que le RP reçoit un jeton contenant les informations nécessaires pour valider si une réauthentification a eu lieu ou non. Le RP peut maintenant consulter la revendication auth_time dans le jeton d’identité pour déterminer si le paramètre max_age qu’il a demandé a bien été respecté. De cette façon, le paramètre max_age=0 est à l’abri du même type d’altération côté client qui pourrait compromettre le paramètre prompt=login.
Gardez à l’esprit qu’il revient uniquement au RP de valider qu’il reçoit un jeton d’identité avec une valeur auth_time appropriée. Cette validation supplémentaire devra être prise en charge par les auteurs d’applications et les frameworks qui utilisent le paramètre max_age.

Utiliser les revendications auth_time

Nous avons établi que la spécification OIDC fournit le paramètre max_age comme moyen de confirmer avec certitude qu’une réauthentification a eu lieu, contrairement à prompt=login. Si vous souhaitez forcer une réauthentification, cela n’offre donc pas d’options très sécurisées :
  • prompt=login: Inclure uniquement le paramètre prompt, sans valider que l’AS a effectivement réauthentifié l’utilisateur.
  • prompt=login & max_age=999999: Inclure une valeur max_age arbitraire afin qu’une revendication auth_time soit présente. Vous pouvez valider qu’une réauthentification a bien eu lieu, mais les paramètres deviennent complexes.
  • max_age=0: Force en pratique l’affichage d’une invite de connexion en utilisant uniquement le paramètre max_age. Notez qu’une mise à jour récente de la spécification a précisé davantage ce paramètre, en indiquant qu’il est en pratique équivalent à prompt=login. Cette option n’est pas vraiment viable, puisqu’elle mélange ce qui devrait être un paramètre d’expérience utilisateur avec un paramètre de maintien de session.
Auth0 a plutôt choisi d’envoyer la revendication auth_time dans le jeton d’identité en réponse à un paramètre de requête prompt=login. Vous pouvez donc utiliser prompt=login ET valider qu’une réauthentification a bien eu lieu.

Exemple de validation de auth_time

Vous devez vous assurer de mettre en place une validation pour confirmer qu’une réauthentification a bien eu lieu. Vous devez vérifier qu’une valeur auth_time valide a été renvoyée.
L’exemple suivant utilise le module passport-auth0-openidconnect pour montrer comment valider la réauthentification. La première méthode (et la plus simple) consiste à ajouter l’option max_age=0 à Auth0OidcStrategy :
JavaScript
var strategy = new Auth0OidcStrategy(
  {
    domain: process.env.AUTH0_DOMAIN,
    clientID: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    callbackURL: process.env.AUTH0_CALLBACK_URL || 'http://localhost:5000/callback',
    max_age: 0
  },
  function(req, issuer, audience, profile, accessToken, refreshToken, params, cb) {
    // Aucune validation supplémentaire requise!
    return cb(null, profile);
  });
Notez qu’aucune étape de validation supplémentaire n’est requise, puisque la stratégie gère déjà la validation du paramètre max_age :
JavaScript
// https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - vérification 8.
if (meta.params.max_age && (!jwtClaims.auth_time || ((meta.timestamp - meta.params.max_age) > jwtClaims.auth_time))) {
  return self.error(new Error('auth_time in id_token not included or too old'));
}
Vous pouvez aussi utiliser prompt=login dans le même contexte, mais puisque la norme n’exige pas qu’un auth_time accompagne la réponse du jeton d’identité, vous devez valider celui-ci manuellement. Le constructeur de stratégie serait donc :
JavaScript
var strategy = new Auth0OidcStrategy(
  {
    domain: process.env.AUTH0_DOMAIN,
    clientID: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    callbackURL: process.env.AUTH0_CALLBACK_URL || 'http://localhost:5000/callback',
    prompt: 'login'
  },
  function(req, issuer, audience, profile, accessToken, refreshToken, params, cb) {
    const tenSecondsAgo = (Date.now() / 1000) - 10;
    if (isNaN(profile.auth_time) || profile.auth_time < tenSecondsAgo) {
      return cb('prompt=login requested, but auth_time is greater than 10 seconds old', null);
    }

    return cb(null, profile);
  });
Contrairement à max_age=0, l’application doit valider manuellement le paramètre auth_time. Pour en savoir plus, consultez Utiliser les claims auth_time.
L’exemple ci-dessus présente une preuve de concept simplifiée (il suppose une authentification au cours des 10 dernières secondes). Idéalement, si vous souhaitez valider qu’une réauthentification a bien eu lieu, vous devrez :
  1. Stocker l’heure à laquelle la demande d’authentification initiale a été effectuée.
  2. Au moment de recevoir la réponse d’authentification, récupérer l’heure d’envoi de la demande.
  3. Comparer l’heure de la demande d’authentification initiale avec la revendication auth_time pour vous assurer que auth_time correspond à un horodatage ultérieur.
Auth0 ne recommande pas d’utiliser l’approche présentée dans l’exemple dans des systèmes de production.

Problèmes connus

Auth0 peut seulement garantir qu’un échange a eu lieu avec le en amont. Il se peut que l’utilisateur se soit réellement connecté auprès d’un fournisseur d’identité tiers, ou qu’il ait déjà une session active et n’ait pas eu à se reconnecter. Dans tous les cas, l’échange d’Auth0 avec le fournisseur d’identité en amont entraînera la mise à jour de auth_time. Auth0 ne prend pas en charge le fait de forcer la réauthentification auprès du fournisseur d’identité en amont, car ce n’est pas pris en charge par tous les fournisseurs. Le diagramme ci-dessous présente un exemple de flux pour un utilisateur qui choisit de se réauthentifier avec une connexion fédérée :
Diagramme illustrant que les connexions fédérées ne forcent pas la réauthentification
Cette méthode suppose que vous utilisez des connexions de base de données. Les fournisseurs d’identité externes peuvent ou non prendre en charge la réauthentification forcée. L’utilisation de prompt=login ou de prompt=consent permet généralement d’indiquer à un fournisseur d’identité externe (social) de réauthentifier un utilisateur, mais Auth0 ne peut pas l’imposer.
Ne vous fiez pas à la vérification côté client (c.-à-d. dans le navigateur) du jeton d’identité ou de auth_time pour bloquer des opérations sensibles.

En savoir plus