quinta-feira, 19 de fevereiro de 2015

Autenticação e sessão em apps Web / REST







Então, você já desenvolve Web há algum tempo, e resolveu trabalhar com REST, certo? Alguém te disse que é mais simples e moderno do que outras soluções, como Java EE, por exemplo. Apesar de estar certo, existem alguns "percalços" para os desavisados, e é isso o que vamos mostrar aqui.



Um servidor REST é "stateless"



O que isso significa? Uma das restrições do REST diz que o Servidor não pode guardar estado de conversação com o Cliente. Ele pode armazenar apenas o estado dos recursos que disponibiliza. Muita gente tentar dar uma "enrolada" básica, forçando a barra para manter estado de comunicação no Servidor, porém, além de ser contra a definição de REST, diminui a escalabilidade da aplicação.

Em uma aplicação REST, o cliente pode ser qualquer coisa, até mesmo um outro serviço. E a função do Servidor é prover os recursos solicitados pelo cliente, efetuando eventuais modificações de estado requisitadas. Ele não deve manter estado de comunicação. Porém, nós estamos acostumados com frameworks nos quais a manutenção de estado de comunicação, também conhecido como "Sessão", é bem natural.

Essa característica (de manter "sessão") cria muitos entraves para a escalabilidade das aplicações e força o Cliente a assumir que está se comunicando diretamente com o Servidor final, o que viola outra das restrições do REST, conhecida como "Sistema em camadas", segundo a qual o Cliente não pode assumir que está se comunicando com um Servidor final, podendo muito estar se comunicando com um Proxy ou um Cache de rede, incapaz de "dialogar" com ele.

Mas o que isso significa?

Significa que você não pode manter uma "sessão" no Servidor. A cada novo "request", o Servidor não se recorda do Cliente. Eu sei que isso cai como uma bomba na cabeça da maioria dos desenvolvedores Web, mas é assim que funciona.

Então, se eu não posso guardar a sessão no Servidor, onde posso colocá-la? Boa questão! Eis as opções:


  • Guardar em um banco de dados e passar o identificador a cada "request";

  • Guardar no próprio Cliente Web;


Guardar em um Banco de Dados é uma excelente opção! Se o Servidor precisar de algum dado enviado em um momento anterior da conversação, pode recuperá-lo do Banco de dados. Então, devemos passar a "chave" dessa sessão a cada novo "request". Essa solução pode escalar bem, dependendo de como configuremos os recursos.

Porém, pensando um pouco, a maioria dos dados que mantemos em sessão, são relacionados apenas com o Usuário que está acessando a aplicação. Logo, por que consumir espaço desnecessário no Servidor? E tem mais: Por que fazer navegações do Cliente ao Servidor apenas porque o estado está guardado remotamente? Não seria melhor guardar no próprio Cliente Web?

Com o advento da "Session storage" (especificação do W3C), temos um novo espaço no Navegador do usuário, onde podemos armazenar dados. A "Session storage" pode conter variáveis Javascript (incluindo objetos JSON), e fica ativa enquanto a janela (ou aba) do navegador estiver aberta. E, diferentemente dos "cookies", os dados armazenados na "Session Storage" jamais são enviados em requests.

Mas e a segurança disso?

Bom, a "Session Storage" fica armazenada no Navegador, e está sujeita às restrições:


  • Só o Usuário que criou, pode acessar

  • É transiente

  • Está sujeita às restrições de Servidor / Porta


Na verdade, o próprio W3C recomenda o armazenamento de dados sensíveis na "Session Storage".

Você não se autentica no Container



Ao contrário de soluções Java EE, por exemplo, você não se autentica com o Container que está hospedando a aplicação. A autenticação é feita através do "header" HTTP "Authorization", e o Servidor verifica a sua presença antes de permitir o acesso.

Você conhece esse "header"! É aquele que costuma aparecer quando o navegador recebe um status "401 Unauthorized"! O comportamento do navegador é abrir uma janela para que você digite o usuário e a senha.

Quando falamos de aplicações REST, podemos ter rotas que são restritas e rotas que são livres. Nas rotas restritas, é necessária a presença do "header" "Authorization", que será verificado pelo Servidor.

Como funciona?

A imagem seguinte apresenta o fluxo dessa comunicação:








  1. O Cliente solicita um recurso restrito;

  2. O Servidor procura o "header" "Authorization" e o valida;

  3. Se o "header" não existir, o Servidor retorna um Status 401 (Unauthorized). Se existir, mas as credenciais forem inválidas, retorna um Statos 403 (Forbidden);

  4. Ao receber um status de erro, o Cliente providencia as credenciais e adiciona o "header" "Authorization", voltando ao primeiro passo;


E como fica isso em uma aplicação MEAN?



Em uma aplicação MEAN, como as que vimos no nosso curso de Mean Stack, podemos implementar isso criando um "middleware" no processamento das nossas rotas, e incluindo validações no lado do Cliente.

Para ajudar, eu migrei uma das aplicações do curso para usar autenticação REST e "Session Storage". Ela está disponível AQUI.

Ela usa um banco de dados MongoDB bem simples, cuja coleção contém apenas o nome do estilo. Ao tentar acessar a página inicial: http://localhost:3000, você receberá uma tela de login:







Ao digitar o usuário e senha ("fulano" e "teste"), você será autenticado pelo Servidor e verá a página da aplicação:







Restrição de rotas

O estilo padrão das aplicações REST é restringir rotas, e não arquivos estáticos. Para restringir rotas, é necessário interceptar o "request" ANTES que elas sejam processadas. No Express.js, fazemos isso com o Middleware.

Um "Middleware" é uma função que recebe o "request" e pode modificar o fluxo da execução. Abra o arquivo "/routes/index.js" e veja como funciona:


/* Middleware */

function verificarAutenticacao(req, res, next) {
 // Paths que precisam de autorizacao:
 if (req.path == '/estilos') {
   var authOk = false;
   var auth = req.headers['authorization'];
   console.log(">>> Header: " + auth);
   if (auth) {
     // Pula o "BASIC ";
     var b64 = new Buffer(auth.substring(6), 'base64');
     var sobj = b64.toString();
     var objeto = JSON.parse(sobj);         
     console.log(">>> Conteudo: " + JSON.stringify(objeto));
     if (objeto.username == "fulano"
         && objeto.senha == "teste") {
         console.log(">>> OK");
         authOk = true;
     }
   }
   if (!authOk) {
     console.log(">>> Falha!");
     res.json(401,"erro");
   }
   else {
     return next();
   }
 }
 else {
   return next();
 }
}

/* Todos os requests devem ser autenticados */
router.use(verificarAutenticacao);


Nosso "Middleware" é uma função (verificarAutenticacao), que recebe o "request", o "response" e um objeto "next", que é o próximo "Middleware" a ser invocado. Se tudo der certo, ela invoca "next()" e o processamento continua. Caso contrário, ela pode construir e enviar respostas diretamente para o Cliente Web.

Eu só protegi uma rota, a que começa com o "path" "/estilo". Então, se o "request" for para outra rota, o "Middleware" vai deixar passar.

Eu estou usando o esquema de autenticação "BASIC", que envia o "header" assim:

Authorization: BASIC

Então, eu preciso decodificar o string de Base 64 para string normal. Depois, eu o transformo em um Objeto JSON novamente. Eu espero um objeto JSON com:

{ "username" : "",
"senha" : ""}

Isto é passado pelo Cliente Web.

Se tudo estiver certo, o "Middleware" retorna "next()" e o processamento do "request" continua, seguindo para a Rota. Caso contrário, eu retorno um status 401.


O comando abaixo faz com que esse "Middleware" seja executado para todas as rotas:

router.use(verificarAutenticacao);

O Cliente, ao solicitar a página "index.html", fará um "request" AJAJ (Asynchronous Javascript And JSON) para obter a lista de estilos (veja o arquivo "/public/javascript/estilocontroller.js":


angular.module('exemplo',[])
 .controller('estilocontroller', ['$scope','$http',
   function($scope, $http) {
     var sessionToken = sessionStorage.token;
     console.log("Session Token: " + sessionToken);
     if (sessionToken) {
       $http.defaults.headers.common.Authorization = 'BASIC ' + sessionToken;
     }
     console.log("Header: " + $http.defaults.headers.common.Authorization);
     debugger;
     $http.get('/estilos').success(function(lista) {
       debugger;
       $scope.estilos = lista;
     })
     .error(function(data, status, headers, config) {
       debugger;
       window.location.href = "/login";
     });
     ;
   }
]);


Antes de fazer o request, ele verifica se existe o token de sessão na "Session Storage". Se existir, ele o recupera e criar um "header" "Authorization" com ele. Caso não exista, ele enviará um "request" sem o "header".

Se der erro na resposta, eu desvio para a rota "/login".

No servidor REST, a rota "/login" retorna uma página de login, com um novo Controller Angular.js (arquivo: "/public/javascripts/logincontroller.js"):


angular.module('loginmodule',[])
 .controller('logincontroller', ['$scope','$http',
   function($scope, $http) {
     $scope.loginUsuario = function() {
       var objeto = {username: $scope.username,
          senha: $scope.senha};
       console.log("Enviando: " + JSON.stringify(objeto));           
       $http.post('/login',objeto)
          .success(function(data,status) {
            var token = btoa("{\"username\" : \"" 
                       + $scope.username 
                       + "\", \"senha\" : \"" 
                       + $scope.senha + "\"}");
            $http.defaults.headers.common.Authorization = 'BASIC ' + token;
            sessionStorage.token = token;
            console.log("Login OK");
            window.location.href = "/";
          })
          .error(function(data, status) {
          });
     };
   }
]);


Ao preencher os campos "username" e "senha", clicando no botão "Login", o usuário invocará o método "loginUsuario", do nosso Controller, que enviará os dados via "POST" para o Servidor. Se a resposta for Ok, então ele armazena o "Token", já convertido em Base 64, na "Session Storage".

Como ele redireciona para "/", a página "index.html" será novamente carregada e tentará novamente obter a lista de estilos, só que, desta vez, haverá um "token" na "Session Storage" e ela vai gerar o "header" "Authorization".

Moral da história



Com esse exemplo, nosso Servidor é "stateless" e o estado da conversação fica apenas no Cliente Web.

E usamos a autenticação REST.

Mas estamos usando BASIC, que é inseguro!

Sim. Mas devemos nos lembrar que quaisquer dados sensíveis só podem ser disponibilizados via HTTPS! Logo, não faz diferença usarmos BASIC.

Se você quiser proteger mais, pode assinar com um certificado digital e colocar uma data de expiração, permitindo ao Servidor que receber o "header", validar sem necessidade de acessar um banco de dados.

Mas dá para usar Single Sign On?

Você está confundindo laranja com banana! Autenticação REST é baseada em HTTP, logo, é compatível com a maioria dos esquemas de single sign on. Autenticação de "Container", representada pela tecnologia JAAS, por exemplo, é outra coisa.

Veja bem, esse é um exemplo muito, mas muuuiiiito simples mesmo! Você pode criar outros esquemas à vontade.