Serverless OAuth com Múltiplos Provedores

oieduardorabelo

Eduardo Rabelo

Posted on February 28, 2021

Serverless OAuth com Múltiplos Provedores

Este exemplo mostra como usar múltiplos provedores OAuth (Google e Github, neste caso) para autenticar em um aplicativo serverless hospedado em Begin.com ou Architect . Autenticação é quem é o usuário, e autorização (também chamada de permissões) é o que o usuário vê ou faz. Ambos são importantes, mas apenas a autenticação é abordada aqui. Nenhuma biblioteca de autenticação, serviço ou SDK do provedor é usado. Com Lambda, menos dependências proporcionam inícios mais rápidos. Begin.com limita especificamente as dependências a 5 MB para encorajar esta prática recomendada.

Para se concentrar no código OAuth, este aplicativo tem o mínimo possível de código. O arquivo app.arc de manifesto abaixo mostra as cinco rotas. A primeira rota / está acessível para convidados e usuários autenticados. A rota /admin só é visível para usuários autenticados e redireciona para /login outros usuários.

# ./app.arc
@app
oauth-example

@http
get /
get /admin
get /auth
get /login
post /logout
Enter fullscreen mode Exit fullscreen mode

Para ver todo o aplicativo, fique à vontade para clonar o repositório oauth-example. Para experimentar por si mesmo, você pode implantá-lo diretamente no Begin.

Implantar para começar

Visão geral do OAuth

O fluxo básico do OAuth é mostrado abaixo. Um usuário solicita um login para o aplicativo e é apresentado com opções para autenticar em qualquer um dos provedores disponíveis. Os links para esses provedores enviam uma solicitação do usuário diretamente para o provedor. Depois de entrar no provedor, uma resposta é enviada ao servidor com um token. O servidor usa esse token para enviar uma solicitação ao provedor para o perfil do usuário usando esse token. Com a resposta, o usuário é autenticado no aplicativo.



diagrama de fluxo básico oauth

Pedido de Login

Quando um usuário solicita /login (ou é redirecionado para lá), a rota gera URLs para cada um dos provedores. Se eles foram redirecionados, um parâmetro "próximo" (isto é /login?next=admin) aponta de volta para a página original. O parâmetro next é verificado em relação a opções válidas (apenas admin aqui) para proteger um usuário de ser direcionado a um site malicioso após a autenticação.

// ./src/http/get-login/index.js
const arc = require('@architect/functions');
const githubOAuthUrl = require('./githubOAuthUrl');
const googleOAuthUrl = require('./googleOAuthUrl');

async function login(req) {
  let finalRedirect = '/';
  if (req.query.next === 'admin') {
    finalRedirect = '/admin';
  }
  const googleUrl = await googleOAuthUrl({ finalRedirect });
  const githubUrl = githubOAuthUrl({ finalRedirect });
  return {
    status: 200,
    html: `<!doctype html>
            <html lang="en">
                <head>
                    <meta charset="utf-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1">
                    <title>login page</title>
                </head>
                <body>
                    <h1>Login</h1></br>
                    <a href="${githubUrl}">Login with Github</a></br>
                    <a href="${googleUrl}">Login with Google</a>
                </body>
            </html>`,
  };
}

exports.handler = arc.http.async(login);
Enter fullscreen mode Exit fullscreen mode

Parâmetro de Estado

O parâmetro de consulta state ajuda a garantir que a solicitação de retorno foi iniciada pelo servidor. Uma opção é gerar um número aleatório seguro armazenado no servidor e, em seguida, verificar com a solicitação de retorno. Neste exemplo, um JSON Web Token (JWT) é usado. O JWT é um objeto assinado criptograficamente que contém o provedor e o local de redirecionamento final (que vem do parâmetro next). Este JWT tem um conjunto de expiração de uma hora. Ele só precisa permanecer válido por tempo suficiente para concluir a autenticação.

// ./src/http/get-login/githubOAuthUrl.js
const jwt = require('jsonwebtoken');
module.exports = function githubOAuthUrl({ finalRedirect }) {
  let client_id = process.env.GITHUB_CLIENT_ID;
  let redirect_uri = encodeURIComponent(process.env.AUTH_REDIRECT);
  let state = jwt.sign(
    {
      provider: 'github',
      finalRedirect,
    },
    process.env.APP_SECRET,
    { expiresIn: '1 hour' }
  );
  let url = `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&state=${state}`;
  return url;
};
Enter fullscreen mode Exit fullscreen mode

URL do Google

A URL do Google é semelhante a função Github, exceto pela solicitação GET na parte superior. A documentação OAuth do Google recomenda verificar o endpoint de autenticação por uma solicitação ao documento "openid-configuration". Se o google alterar o caminho real do endpoint, ele será atualizado neste documento. Este documento é armazenado em cache agressivamente e a solicitação geralmente será retornada de lá.

// ./src/http/get-login/googleOAuthUrl.js
const tiny = require('tiny-json-http');
const jwt = require('jsonwebtoken');

module.exports = async function googleOAuthUrl({ finalRedirect }) {
  const googleDiscoveryDoc = await tiny.get({
    url: 'https://accounts.google.com/.well-known/openid-configuration',
    headers: { Accept: 'application/json' },
  });
  const authorization_endpoint = googleDiscoveryDoc.body.authorization_endpoint;
  const state = await jwt.sign(
    {
      provider: 'google',
      finalRedirect,
    },
    process.env.APP_SECRET,
    { expiresIn: '1 hour' }
  );
  const options = {
    access_type: 'online',
    scope: ['profile', 'email'],
    redirect_uri: process.env.AUTH_REDIRECT,
    response_type: 'code',
    client_id: process.env.GOOGLE_CLIENT_ID,
  };
  const url = `${authorization_endpoint}?access_type=${options.access_type}&scope=${encodeURIComponent(
    options.scope.join(' ')
  )}&redirect_uri=${encodeURIComponent(options.redirect_uri)}&response_type=${options.response_type}&client_id=${
    options.client_id
  }&state=${state}`;

  return url;
};
Enter fullscreen mode Exit fullscreen mode

Auth Redirect do Provedor

Depois que o usuário fazer login com o provedor escolhido (Google ou Github), ele será redirecionado para a rota /auth com um conjunto de parâmetros code e state. O estado deve ser exatamente o mesmo estado que foi enviado ao provedor. O parâmetro code é um token usado para acessar as informações do perfil do usuário. O manipulador da rota /auth é mostrado abaixo. Ele decodifica o estado do JWT para verificar se a solicitação foi iniciada pelo aplicativo e para determinar qual provedor enviou o código de autorização.

// ./src/http/get-auth/index.js
const arc = require('@architect/functions');
const githubAuth = require('./githubAuth');
const googleAuth = require('./googleAuth');
const jwt = require('jsonwebtoken');

async function auth(req) {
  let account = {};
  let state;
  if (req.query.code && req.query.state) {
    try {
      state = jwt.verify(req.query.state, process.env.APP_SECRET);
      if (state.provider === 'google') {
        account.google = await googleAuth(req);
        if (!account.google.email) {
          throw new Error();
        }
      } else if (state.provider === 'github') {
        account.github = await githubAuth(req);
        if (!account.github.login) {
          throw new Error();
        }
      } else {
        throw new Error();
      }
    } catch (err) {
      return {
        status: 401,
        body: 'not authorized',
      };
    }
    return {
      session: { account },
      status: 302,
      location: state.finalRedirect,
    };
  } else {
    return {
      status: 401,
      body: 'not authorized',
    };
  }
}

exports.handler = arc.http.async(auth);
Enter fullscreen mode Exit fullscreen mode

Solicitar perfil de usuário

A etapa final na sequência OAuth é obter o perfil do usuário. Para o Github, uma solicitação POST é enviada usando o code junto com o client_id e client_secret. O Github responde com um token de acesso. Esse token é então usado para fazer uma solicitação GET para o perfil do usuário. Com isso, o perfil do usuário é finalmente retornado e armazenado na sessão do Architect / Begin. O usuário agora está autenticado para qualquer solicitações adicionais do aplicativo.

// ./src/http/get-auth/githubAuth.js
const tiny = require('tiny-json-http');

module.exports = async function githubAuth(req) {
  try {
    let result = await tiny.post({
      url: 'https://github.com/login/oauth/access_token',
      headers: { Accept: 'application/json' },
      data: {
        code: req.query.code,
        client_id: process.env.GITHUB_CLIENT_ID,
        client_secret: process.env.GITHUB_CLIENT_SECRET,
        redirect_uri: process.env.AUTH_REDIRECT,
      },
    });
    let token = result.body.access_token;
    let user = await tiny.get({
      url: `https://api.github.com/user`,
      headers: {
        Authorization: `token ${token}`,
        Accept: 'application/json',
      },
    });
    return {
      name: user.body.name,
      login: user.body.login,
      id: user.body.id,
      url: user.body.url,
      avatar: user.body.avatar_url,
    };
  } catch (err) {
    return {
      error: err.message,
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

O Google também exige a verificação do endpoint do token antes da autenticação final (semelhante à etapa inicial). Novamente, essa resposta é armazenada em cache agressivamente para minimizar solicitações desnecessárias. Para obter o perfil do usuário com o Github usamos uma chamada POST para o token de acesso e um GET com esse token para o perfil do usuário. O Google combina esses dois. Com uma solicitação POST, recebemos um id_token que é um JWT com o perfil do usuário. O JWT é então decodificado para obter as informações do perfil do usuário.

// ./src/http/get-auth/googleAuth.js
const tiny = require('tiny-json-http');
const jwt = require('jsonwebtoken');

module.exports = async function googleAuth(req) {
  let googleDiscoveryDoc = await tiny.get({
    url: 'https://accounts.google.com/.well-known/openid-configuration',
    headers: { Accept: 'application/json' },
  });
  let token_endpoint = googleDiscoveryDoc.body.token_endpoint;

  let result = await tiny.post({
    url: token_endpoint,
    headers: { Accept: 'application/json' },
    data: {
      code: req.query.code,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: process.env.AUTH_REDIRECT,
      grant_type: 'authorization_code',
    },
  });
  return jwt.decode(result.body.id_token);
};
Enter fullscreen mode Exit fullscreen mode

Configurando OAuth nos provedores

Para se autenticar no Google e no Github, você precisa configurar isso com os dois provedores.

Configuração do Github

Para Github.com, navegue até:

Configurações -> Configurações do Desenvolvedor -> Aplicativos OAuth -> Novo Aplicativo OAuth.

A partir daí, preencha o formulário conforme necessário. Certifique-se de que o url de retorno de chamada corresponda ao domínio completo e ao caminho do seu aplicativo. O domínio para teste e produção pode ser encontrado nas configurações no Begin. Mais detalhes podem ser encontrados no Github Docs .

Configuração do Google

O console do Google é mais complicado de navegar. Comece inscrevendo-se em uma conta de desenvolvedor em https://console.cloud.google.com/ . Configure um novo projeto e vá para o painel "API e Serviços". Configure a "Tela de consentimento OAuth" para usuários externos. Em seguida, escolha Credenciais -> Criar Credenciais -> OAuth Client ID. Siga a configuração do "aplicativo da web" e insira o URL de redirecionamento e outras informações para seu aplicativo.

Fluxo OAuth completo

O diagrama de fluxo completo do OAuth é mostrado abaixo. As etapas do Google são mostradas em vermelho e o Github em azul.

diagrama de fluxo oauth

Créditos

💖 💪 🙅 🚩
oieduardorabelo
Eduardo Rabelo

Posted on February 28, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related