OAuth2 Lembre-se de mim com o token de atualização
1. Visão geral
Neste artigo, adicionaremos uma funcionalidade “Lembre-se de mim” a um aplicativo protegidoOAuth 2, aproveitando o token de atualizaçãoOAuth 2.
Este artigo é uma continuação de nossa série sobre o uso deOAuth 2 para proteger uma API Spring REST, que é acessada por meio de um clienteAngularJS. Para configurar o Authorization Server, Resource Server e front-end Client, você pode seguirthe introductory article.
Então, você pode continuar com nosso artigo sobrehandling the refresh token usando um proxyZuul.
2. Token de acesso OAuth 2 e token de atualização
Primeiro, vamos fazer uma rápida recapitulação dos tokensOAuth 2 e como eles podem ser usados.
Em uma primeira tentativa de autenticação usando o tipo de concessãopassword, o usuário precisa enviar um nome de usuário e senha válidos, bem como o ID do cliente e o segredo. Se a solicitação de autenticação for bem-sucedida, o servidor retornará uma resposta do formulário:
{
"access_token": "2e17505e-1c34-4ea6-a901-40e49ba786fa",
"token_type": "bearer",
"refresh_token": "e5f19364-862d-4212-ad14-9d6275ab1a62",
"expires_in": 59,
"scope": "read write",
}
Podemos ver que a resposta do servidor contém um token de acesso e um token de atualização. O token de acesso será usado para chamadas API subsequentes que requerem autenticação, enquantothe purpose of the refresh token is to obtain a new valid access token ou apenas revogam a anterior.
Para receber um novo token de acesso usando o tipo de concessãorefresh_token, o usuário não precisa mais inserir suas credenciais, mas apenas o ID do cliente, segredo e, claro, o token de atualização.
The goal of using two types of tokens is to enhance user security. Normalmente, o token de acesso tem um período de validade mais curto, de modo que, se um invasor obtiver o token de acesso, ele terá um tempo limitado para usá-lo. Por outro lado, se o token de atualização for comprometido, isso será inútil, pois o ID e o segredo do cliente também são necessários.
Outro benefício dos tokens de atualização é que ele permite revogar o token de acesso e não enviar outro se o usuário exibir um comportamento incomum, como efetuar login a partir de um novo IP.
3. Funcionalidade Lembrar-me com Tokens de Atualização
Os usuários geralmente acham útil ter a opção de preservar sua sessão, pois não precisam inserir suas credenciais toda vez que acessam o aplicativo.
Como o token de acesso tem um tempo de validade mais curto, podemos usar tokens de atualização para gerar novos tokens de acesso e evitar a necessidade de solicitar credenciais ao usuário sempre que um token de acesso expirar.
Nas próximas seções, discutiremos duas maneiras de implementar essa funcionalidade:
-
primeiro, interceptando qualquer solicitação de usuário que retorne um código de status 401, o que significa que o token de acesso é inválido. Quando isso ocorrer, se o usuário tiver marcado a opção “lembrar-me”, emitiremos automaticamente uma solicitação para um novo token de acesso usando o tipo de concessãorefresh_token e, em seguida, executaremos a solicitação inicial novamente.
-
em segundo lugar, podemos atualizar o token de acesso de forma proativa - enviaremos uma solicitação para atualizar o token alguns segundos antes de expirar
A segunda opção tem a vantagem de que as solicitações do usuário não serão atrasadas.
4. Armazenamento do token de atualização
No artigo anterior, adicionamos umCustomPostZuulFilter que intercepta solicitações ao servidorOAuth, extrai o token de atualização enviado de volta na autenticação e o armazena em um cookie do lado do servidor:
@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);
//...
}
}
A seguir, vamos adicionar uma caixa de seleção em nosso formulário de login que tem uma ligação de dados à variávelloginData.remember:
Nosso formulário de login agora exibirá uma caixa de seleção adicional:
O objetologinData é enviado com a solicitação de autenticação, portanto, incluirá o parâmetroremember. Antes que a solicitação de autenticação seja enviada, definiremos um cookie chamadoremember com base no parâmetro:
function obtainAccessToken(params){
if (params.username != null){
if (params.remember != null){
$cookies.put("remember","yes");
}
else {
$cookies.remove("remember");
}
}
//...
}
Como consequência, verificaremos esse cookie para determinar se devemos tentar atualizar o token de acesso ou não, dependendo se o usuário deseja ser lembrado ou não.
5. Atualizando Tokens Interceptando 401 Respostas
Para interceptar solicitações que retornam com uma resposta 401, vamos modificar nosso aplicativoAngularJS para adicionar um interceptor com uma funçãoresponseError:
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;
}]);
Nossa função verifica se o status é 401 - o que significa que o token de acesso é inválido e, se for o caso, tenta usar o token de atualização para obter um novo token de acesso válido.
Se isso for bem-sucedido, a função continuará tentando novamente a solicitação inicial que resultou no erro 401. Isso garante uma experiência perfeita para o usuário.
Vamos dar uma olhada mais de perto no processo de atualização do token de acesso. Primeiro, inicializaremos as variáveis necessárias:
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)
}
Você pode ver a variávelreq que usaremos para enviar uma solicitação POST para o endpoint / oauth / token, com o parâmetrogrant_type=refresh_token.
A seguir, vamos usar o módulo$http que injetamos para enviar a solicitação. Se a solicitação for bem-sucedida, definiremos um novo cabeçalhoAuthentication com o novo valor do token de acesso, bem como um novo valor para o cookieaccess_token. Se a solicitação falhar, o que pode acontecer se o token de atualização também expirar, o usuário será redirecionado para a página de login:
$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";
}
);
O token de atualização é adicionado à solicitação porCustomPreZuulFilter que implementamos no artigo anterior:
@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));
}
//...
}
}
Além de definir o interceptor, precisamos registrá-lo com o$httpProvider:
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('rememberMeInterceptor');
}]);
6. Atualizando Tokens Proativamente
Outra maneira de implementar a funcionalidade "lembre-se de mim" é solicitando um novo token de acesso antes que o atual expire.
Ao receber um token de acesso, a resposta JSON contém um valorexpires_in que especifica o número de segundos durante os quais o token será válido.
Vamos salvar esse valor em um cookie para cada autenticação:
$cookies.put("validity", data.data.expires_in);
Então, para enviar uma solicitação de atualização, vamos usar o serviçoAngularJS $timeout para agendar uma chamada de atualização 10 segundos antes que o token expire:
if ($cookies.get("remember") == "yes"){
var validity = $cookies.get("validity");
if (validity >10) validity -= 10;
$timeout( function(){ $scope.refreshAccessToken(); }, validity * 1000);
}
7. Conclusão
Neste tutorial, exploramos duas maneiras de implementar a funcionalidade "Lembre-se de mim" com um aplicativo OAuth2 e um front-endAngularJS.
O código-fonte completo dos exemplos pode ser encontradoover on GitHub. Você pode acessar a página de login com a funcionalidade “lembrar de mim” no URL/login_remember.