OAuth2 pour une API REST Spring - Gestion du jeton d’actualisation dans AngularJS

OAuth2 pour une API REST de printemps - Gestion du jeton d'actualisation dans AngularJS

1. Vue d'ensemble

Dans ce didacticiel, nous continuerons à explorer le flux de mots de passe OAuth que nous avons commencé à rassembler dans plus deour previous article et nous nous concentrerons sur la façon de gérer le jeton d'actualisation dans une application AngularJS.

2. Expiration du jeton d'accès

Tout d’abord, rappelez-vous que le client obtenait un jeton d’accès lorsque l’utilisateur se connectait à l’application:

function obtainAccessToken(params) {
    var req = {
        method: 'POST',
        url: "oauth/token",
        headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
        data: $httpParamSerializer(params)
    }
    $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");
            window.location.href = "login";
        });
}

Notez comment notre jeton d'accès est stocké dans un cookie qui expirera en fonction de l'expiration du jeton lui-même.

Ce qu'il est important de comprendre, c'est quethe cookie itself is only used for storage et cela ne pilote rien d'autre dans le flux OAuth. Par exemple, le navigateur n'enverra jamais automatiquement le cookie au serveur avec des demandes.

Notez également comment nous appelons réellement cette fonctionobtainAccessToken():

$scope.loginData = {
    grant_type:"password",
    username: "",
    password: "",
    client_id: "fooClientIdPassword"
};

$scope.login = function() {
    obtainAccessToken($scope.loginData);
}

3. Le proxy

Nous allons maintenant avoir un proxy Zuul s'exécutant dans l'application frontale et essentiellement assis entre le client frontal et le serveur d'autorisation.

Configurons les routes du proxy:

zuul:
  routes:
    oauth:
      path: /oauth/**
      url: http://localhost:8081/spring-security-oauth-server/oauth

Ce qui est intéressant ici, c'est que nous ne transmettons que le trafic au serveur d'autorisation et rien d'autre. Nous n'avons vraiment besoin que du proxy pour entrer lorsque le client obtient de nouveaux jetons.

Si vous voulez passer en revue les bases de Zuul, lisez rapidementthe main Zuul article.

4. Un filtre Zuul qui effectue une authentification de base

La première utilisation du proxy est simple - au lieu de révéler notre application «client secret» en javascript, nous utiliserons un pré-filtre Zuul pour ajouter un en-tête d'autorisation pour accéder aux demandes de jetons:

@Component
public class CustomPreZuulFilter extends ZuulFilter {
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.getRequest().getRequestURI().contains("oauth/token")) {
            byte[] encoded;
            try {
                encoded = Base64.encode("fooClientIdPassword:secret".getBytes("UTF-8"));
                ctx.addZuulRequestHeader("Authorization", "Basic " + new String(encoded));
            } catch (UnsupportedEncodingException e) {
                logger.error("Error occured in pre filter", e);
            }
        }
        return null;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public int filterOrder() {
        return -2;
    }

    @Override
    public String filterType() {
        return "pre";
    }
}

Gardez maintenant à l'esprit que cela n'ajoute aucune sécurité supplémentaire et que la seule raison pour laquelle nous le faisons est que le point de terminaison du jeton est sécurisé avec l'authentification de base à l'aide des informations d'identification du client.

Du point de vue de la mise en œuvre, le type de filtre mérite particulièrement d'être noté. Nous utilisons un type de filtre "pré" pour traiter la demande avant de la transmettre.

Passons aux choses amusantes.

Ce que nous prévoyons de faire ici, c'est que le client obtienne le jeton d'actualisation sous forme de cookie. Pas seulement un cookie normal, mais un cookie sécurisé, uniquement HTTP avec un chemin très limité (/oauth/token).

Nous allons configurer un post-filtre Zuul pour extraire le jeton d'actualisation du corps JSON de la réponse et le définir dans le cookie:

@Component
public class CustomPostZuulFilter extends ZuulFilter {
    private ObjectMapper mapper = new ObjectMapper();

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        try {
            InputStream is = ctx.getResponseDataStream();
            String responseBody = IOUtils.toString(is, "UTF-8");
            if (responseBody.contains("refresh_token")) {
                Map responseMap = mapper.readValue(
                  responseBody, new TypeReference>() {});
                String refreshToken = responseMap.get("refresh_token").toString();
                responseMap.remove("refresh_token");
                responseBody = mapper.writeValueAsString(responseMap);

                Cookie cookie = new Cookie("refreshToken", refreshToken);
                cookie.setHttpOnly(true);
                cookie.setSecure(true);
                cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
                cookie.setMaxAge(2592000); // 30 days
                ctx.getResponse().addCookie(cookie);
            }
            ctx.setResponseBody(responseBody);
        } catch (IOException e) {
            logger.error("Error occured in zuul post filter", e);
        }
        return null;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public String filterType() {
        return "post";
    }
}

Quelques choses intéressantes à comprendre ici:

  • Nous avons utilisé un post-filtre Zuul pour lire la réponse et lesextract refresh token

  • Nous avons supprimé la valeur durefresh_token de la réponse JSON pour nous assurer qu'elle n'est jamais accessible au frontal en dehors du cookie

  • Nous définissons l'âge maximum du cookie à30 days - car cela correspond à l'heure d'expiration du jeton

Maintenant que nous avons le jeton d'actualisation dans le cookie, lorsque l'application frontale AngularJS tente de déclencher une actualisation de jeton, elle va envoyer la demande à/oauth/token et le navigateur enverra bien sûr ce cookie.

Nous allons donc maintenant avoir un autre filtre dans le proxy qui extraira le jeton d'actualisation du cookie et l'enverra en tant que paramètre HTTP - afin que la requête soit valide:

public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ...
    HttpServletRequest req = ctx.getRequest();
    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));
    }
    ...
}

private String extractRefreshToken(HttpServletRequest req) {
    Cookie[] cookies = req.getCookies();
    if (cookies != null) {
        for (int i = 0; i < cookies.length; i++) {
            if (cookies[i].getName().equalsIgnoreCase("refreshToken")) {
                return cookies[i].getValue();
            }
        }
    }
    return null;
}

Et voici nosCustomHttpServletRequest - utilisés pourinject our refresh token parameters:

public class CustomHttpServletRequest extends HttpServletRequestWrapper {
    private Map additionalParams;
    private HttpServletRequest request;

    public CustomHttpServletRequest(
      HttpServletRequest request, Map additionalParams) {
        super(request);
        this.request = request;
        this.additionalParams = additionalParams;
    }

    @Override
    public Map getParameterMap() {
        Map map = request.getParameterMap();
        Map param = new HashMap();
        param.putAll(map);
        param.putAll(additionalParams);
        return param;
    }
}

Encore une fois, beaucoup de notes importantes sur la mise en œuvre ici:

  • Le proxy extrait le jeton d'actualisation à partir du cookie

  • Il le définit ensuite dans le paramètrerefresh_token

  • Il définit également legrant_type surrefresh_token

  • S'il n'y a pas de cookierefreshToken (expiré ou première connexion) - la demande de jeton d'accès sera redirigée sans changement

7. Actualisation du jeton d'accès à partir d'AngularJS

Enfin, modifions notre application frontale simple et utilisons en fait l'actualisation du jeton:

Voici notre fonctionrefreshAccessToken():

$scope.refreshAccessToken = function() {
    obtainAccessToken($scope.refreshData);
}

Et voici nos$scope.refreshData:

$scope.refreshData = {grant_type:"refresh_token"};

Notez comment nous utilisons simplement la fonctionobtainAccessToken existante - et nous lui transmettons simplement différentes entrées.

Notez également que nous n’ajoutons pas nous-mêmes lesrefresh_token, car cela sera pris en charge par le filtre Zuul.

8. Conclusion

Dans ce didacticiel OAuth, nous avons appris à stocker le jeton d'actualisation dans une application client AngularJS, à actualiser un jeton d'accès expiré et à exploiter le proxy Zuul pour toutes ces opérations.

Lesfull implementation de ce didacticiel se trouvent dansthe github project - il s'agit d'un projet basé sur Eclipse, il devrait donc être facile à importer et à exécuter tel quel.