DEV Community

Eduardo Rabelo
Eduardo Rabelo

Posted on

2 2

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

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

DEV is better (more customized, reading settings like dark mode etc) when you're signed in!

Okay