OAuth2 para uma API REST do Spring - manipule o token de atualização no AngularJS

OAuth2 para uma API REST do Spring - manipule o token de atualização no AngularJS

1. Visão geral

Neste tutorial, continuaremos explorando o fluxo de senha OAuth que começamos a reunir em maisour previous articlee nos concentraremos em como lidar com o token de atualização em um aplicativo AngularJS.

2. Expiração de token de acesso

Primeiro, lembre-se de que o cliente estava obtendo um token de acesso quando o usuário estava efetuando login no aplicativo:

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";
        });
}

Observe como nosso token de acesso é armazenado em um cookie que expira com base em quando o próprio token expira.

O que é importante entender é quethe cookie itself is only used for storagee não direciona mais nada no fluxo de OAuth. Por exemplo, o navegador nunca envia automaticamente o cookie ao servidor com solicitações.

Observe também como realmente chamamos esta funçãoobtainAccessToken():

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

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

3. O Proxy

Agora teremos um proxy Zuul em execução no aplicativo front-end e basicamente entre o cliente front-end e o servidor de autorização.

Vamos configurar as rotas do proxy:

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

O que é interessante aqui é que estamos apenas fazendo proxy do tráfego para o servidor de autorização e nada mais. Nós realmente precisamos que o proxy entre quando o cliente estiver obtendo novos tokens.

Se você quiser repassar o básico do Zuul, faça uma leitura rápida dethe main Zuul article.

4. Um filtro Zuul que faz autenticação básica

O primeiro uso do proxy é simples - em vez de revelar nosso aplicativo “client secret” em javascript, usaremos um pré-filtro Zuul para adicionar o cabeçalho de autorização para acessar solicitações de token:

@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";
    }
}

Agora, lembre-se de que isso não adiciona nenhuma segurança extra e a única razão de estarmos fazendo isso é porque o ponto de extremidade do token é protegido com autenticação básica usando credenciais de cliente.

Do ponto de vista da implementação, vale a pena notar o tipo de filtro. Estamos usando um tipo de filtro "pré" para processar a solicitação antes de encaminhá-la.

Para as coisas divertidas.

O que estamos planejando fazer aqui é que o cliente obtenha o token de atualização como um cookie. Não apenas um cookie normal, mas um cookie seguro, somente HTTP, com um caminho muito limitado (/oauth/token).

Vamos configurar um pós-filtro Zuul para extrair o token de atualização do corpo JSON da resposta e defini-lo no 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";
    }
}

Algumas coisas interessantes para entender aqui:

  • Usamos um pós-filtro Zuul para ler a resposta eextract refresh token

  • Removemos o valor derefresh_token da resposta JSON para garantir que nunca fique acessível para o front end fora do cookie

  • Definimos a idade máxima do cookie para30 days - uma vez que corresponde ao tempo de expiração do token

Agora que temos o token de atualização no cookie, quando o aplicativo AngularJS de front-end tentar acionar uma atualização de token, ele enviará a solicitação em/oauth/tokene o navegador, é claro, enviará esse cookie.

Portanto, agora teremos outro filtro no proxy que extrairá o token de atualização do cookie e o enviará como um parâmetro HTTP - para que a solicitação seja válida:

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;
}

E aqui está nossoCustomHttpServletRequest - usado parainject 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;
    }
}

Novamente, muitas notas importantes de implementação aqui:

  • O proxy está extraindo o token de atualização do cookie

  • Em seguida, ele o define no parâmetrorefresh_token

  • Também está definindogrant_type pararefresh_token

  • Se não houver cookierefreshToken (expirado ou primeiro login) - então a solicitação do token de acesso será redirecionada sem alteração

7. Atualizando o token de acesso do AngularJS

Por fim, vamos modificar nosso aplicativo front-end simples e usar a atualização do token:

Aqui está nossa funçãorefreshAccessToken():

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

E aqui nosso$scope.refreshData:

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

Observe como estamos simplesmente usando a funçãoobtainAccessToken existente - e apenas passando diferentes entradas para ela.

Observe também que não estamos adicionandorefresh_token nós mesmos - já que isso será tratado pelo filtro Zuul.

8. Conclusão

Neste tutorial do OAuth, aprendemos como armazenar o token de atualização em um aplicativo cliente AngularJS, como atualizar um token de acesso expirado e como aproveitar o proxy Zuul para tudo isso.

Ofull implementation deste tutorial pode ser encontrado emthe github project - este é um projeto baseado em Eclipse, portanto, deve ser fácil de importar e executar como está.