Serverless OAuth com Múltiplos Provedores
Eduardo Rabelo
Posted on February 28, 2021
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
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.
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.
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);
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;
};
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;
};
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);
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,
};
}
};
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);
};
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.
Créditos
- Serverless OAuth with Multiple Providers, escrito originalmente por Ryan Bethel.
Posted on February 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.