OAuth2 Se souvenir de moi avec le jeton de rafraîchissement

OAuth2 Se souvenir de moi avec le jet de rafraîchissement

1. Vue d'ensemble

Dans cet article, nous allons ajouter une fonctionnalité «Se souvenir de moi» à une application sécuriséeOAuth 2, en tirant parti du jeton d'actualisationOAuth 2.

Cet article est une continuation de notre série sur l'utilisation deOAuth 2 pour sécuriser une API Spring REST, accessible via un clientAngularJS. Pour configurer le serveur d'autorisation, le serveur de ressources et le client frontal, vous pouvez suivrethe introductory article.

Ensuite, vous pouvez continuer avec notre article surhandling the refresh token en utilisant un proxyZuul.

2. Jeton d'accès OAuth 2 et jeton d'actualisation

Tout d'abord, récapitulons brièvement les jetonsOAuth 2 et la manière dont ils peuvent être utilisés.

Lors d'une première tentative d'authentification à l'aide du type d'accordpassword, l'utilisateur doit envoyer un nom d'utilisateur et un mot de passe valides, ainsi que l'ID client et le secret. Si la demande d'authentification aboutit, le serveur renvoie une réponse du formulaire:

{
    "access_token": "2e17505e-1c34-4ea6-a901-40e49ba786fa",
    "token_type": "bearer",
    "refresh_token": "e5f19364-862d-4212-ad14-9d6275ab1a62",
    "expires_in": 59,
    "scope": "read write",
}

Nous pouvons voir que la réponse du serveur contient à la fois un jeton d'accès et un jeton d'actualisation. Le jeton d'accès sera utilisé pour les appels d'API ultérieurs qui nécessitent une authentification, tandis quethe purpose of the refresh token is to obtain a new valid access token ou tout simplement révoquer le précédent.

Pour recevoir un nouveau jeton d'accès en utilisant le type d'octroirefresh_token, l'utilisateur n'a plus besoin de saisir ses informations d'identification, mais uniquement l'ID client, le secret et bien sûr le jeton d'actualisation.

The goal of using two types of tokens is to enhance user security. Généralement, le jeton d'accès a une période de validité plus courte, de sorte que si un attaquant obtient le jeton d'accès, il dispose d'un temps limité pour l'utiliser. D'un autre côté, si le jeton d'actualisation est compromis, cela est inutile car l'identifiant et le secret du client sont également nécessaires.

Un autre avantage des jetons d'actualisation est qu'ils permettent de révoquer le jeton d'accès et de ne pas en renvoyer un autre si l'utilisateur présente un comportement inhabituel, tel que la connexion à partir d'une nouvelle adresse IP.

3. Fonctionnalité Remember-Me avec des jetons d'actualisation

Les utilisateurs trouvent généralement utile d'avoir la possibilité de conserver leur session, car ils n'ont pas besoin de saisir leurs informations d'identification chaque fois qu'ils accèdent à l'application.

Comme la durée de validité du jeton d'accès est plus courte, nous pouvons utiliser des jetons d'actualisation pour générer de nouveaux jetons d'accès et éviter de demander à l'utilisateur ses informations d'identification à chaque expiration du jeton d'accès.

Dans les sections suivantes, nous aborderons deux façons de mettre en œuvre cette fonctionnalité:

  • Tout d'abord, en interceptant toute requête utilisateur renvoyant un code d'état 401, ce qui signifie que le jeton d'accès n'est pas valide. Lorsque cela se produit, si l'utilisateur a coché l'option "se souvenir de moi", nous émettrons automatiquement une demande pour un nouveau jeton d'accès en utilisant le type d'accord derefresh_token, puis exécutons à nouveau la demande initiale.

  • Deuxièmement, nous pouvons actualiser le jeton d'accès de manière proactive - nous enverrons une demande d'actualisation du jeton quelques secondes avant son expiration

La deuxième option présente l’avantage que les demandes de l’utilisateur ne seront pas retardées.

4. Stockage du jeton d'actualisation

Dans l'article précédent, nous avons ajouté unCustomPostZuulFilter qui intercepte les requêtes au serveurOAuth, extrait le jeton d'actualisation renvoyé lors de l'authentification et le stocke dans un cookie côté serveur:

@Component
public class CustomPostZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        Cookie cookie = new Cookie("refreshToken", refreshToken);
        cookie.setHttpOnly(true);
        cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
        cookie.setMaxAge(2592000); // 30 days
        ctx.getResponse().addCookie(cookie);
        //...
    }
}

Ensuite, ajoutons une case à cocher sur notre formulaire de connexion qui a une liaison de données avec la variableloginData.remember:


Notre formulaire de connexion affichera maintenant une case à cocher supplémentaire:

image

L'objetloginData est envoyé avec la demande d'authentification, il inclura donc le paramètreremember. Avant l'envoi de la demande d'authentification, nous allons définir un cookie nomméremember en fonction du paramètre:

function obtainAccessToken(params){
    if (params.username != null){
        if (params.remember != null){
            $cookies.put("remember","yes");
        }
        else {
            $cookies.remove("remember");
        }
    }
    //...
}

En conséquence, nous vérifierons ce cookie pour déterminer si nous devons essayer d'actualiser le jeton d'accès ou non, selon que l'utilisateur souhaite être mémorisé ou non.

5. Actualisation des jetons en interceptant 401 réponses

Pour intercepter les requêtes qui reviennent avec une réponse 401, modifions notre applicationAngularJS pour ajouter un intercepteur avec une fonctionresponseError:

app.factory('rememberMeInterceptor', ['$q', '$injector', '$httpParamSerializer',
  function($q, $injector, $httpParamSerializer) {
    var interceptor = {
        responseError: function(response) {
            if (response.status == 401){

                // refresh access token

                // make the backend call again and chain the request
                return deferred.promise.then(function() {
                    return $http(response.config);
                });
            }
            return $q.reject(response);
        }
    };
    return interceptor;
}]);

Notre fonction vérifie si le statut est 401 - ce qui signifie que le jeton d'accès est invalide. Si tel est le cas, tente d'utiliser le jeton d'actualisation afin d'obtenir un nouveau jeton d'accès valide.

Si cela réussit, la fonction continue d'essayer de nouveau la requête initiale, ce qui a entraîné l'erreur 401. Cela garantit une expérience transparente pour l'utilisateur.

Examinons de plus près le processus d'actualisation du jeton d'accès. Tout d'abord, nous allons initialiser les variables nécessaires:

var $http = $injector.get('$http');
var $cookies = $injector.get('$cookies');
var deferred = $q.defer();

var refreshData = {grant_type:"refresh_token"};

var req = {
    method: 'POST',
    url: "oauth/token",
    headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
    data: $httpParamSerializer(refreshData)
}

Vous pouvez voir la variablereq que nous utiliserons pour envoyer une requête POST au point de terminaison / oauth / token, avec le paramètregrant_type=refresh_token.

Ensuite, utilisons le module$http que nous avons injecté pour envoyer la requête. Si la demande aboutit, nous définirons un nouvel en-têteAuthentication avec la nouvelle valeur du jeton d'accès, ainsi qu'une nouvelle valeur pour le cookieaccess_token. Si la demande échoue, ce qui peut arriver si le jeton d'actualisation expire également, l'utilisateur est alors redirigé vers la page de connexion:

$http(req).then(
    function(data){
        $http.defaults.headers.common.Authorization= 'Bearer ' + data.data.access_token;
        var expireDate = new Date (new Date().getTime() + (1000 * data.data.expires_in));
        $cookies.put("access_token", data.data.access_token, {'expires': expireDate});
        window.location.href="index";
    },function(){
        console.log("error");
        $cookies.remove("access_token");
        window.location.href = "login";
    }
);

Le jeton d'actualisation est ajouté à la demande par lesCustomPreZuulFilter que nous avons implémentés dans l'article précédent:

@Component
public class CustomPreZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        String refreshToken = extractRefreshToken(req);
        if (refreshToken != null) {
            Map param = new HashMap();
            param.put("refresh_token", new String[] { refreshToken });
            param.put("grant_type", new String[] { "refresh_token" });

            ctx.setRequest(new CustomHttpServletRequest(req, param));
        }
        //...
    }
}

En plus de définir l'intercepteur, nous devons l'enregistrer avec les$httpProvider:

app.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push('rememberMeInterceptor');
}]);

6. Actualisation proactive des jetons

Un autre moyen d'implémenter la fonctionnalité «souvenez-vous de moi» consiste à demander un nouveau jeton d'accès avant l'expiration du jeton actuel.

Lors de la réception d'un jeton d'accès, la réponse JSON contient une valeurexpires_in qui spécifie le nombre de secondes pendant lesquelles le jeton sera valide.

Enregistrons cette valeur dans un cookie pour chaque authentification:

$cookies.put("validity", data.data.expires_in);

Ensuite, pour envoyer une demande d'actualisation, utilisons le serviceAngularJS $timeout pour planifier un appel d'actualisation 10 secondes avant l'expiration du jeton:

if ($cookies.get("remember") == "yes"){
    var validity = $cookies.get("validity");
    if (validity >10) validity -= 10;
    $timeout( function(){ $scope.refreshAccessToken(); }, validity * 1000);
}

7. Conclusion

Dans ce didacticiel, nous avons exploré deux façons de mettre en œuvre la fonctionnalité "Se souvenir de moi" avec une application OAuth2 et une interfaceAngularJS.

Le code source complet des exemples peut être trouvéover on GitHub. Vous pouvez accéder à la page de connexion avec la fonctionnalité «se souvenir de moi» à l'URL/login_remember.