UNPKG

@betha-plataforma/oauth

Version:

Biblioteca JavaScript para lidar com o fluxo do OAuth 2.0 em aplicações Web, com suporte a TypeScript.

522 lines (396 loc) 19.3 kB
# @betha-plataforma/oauth Biblioteca JavaScript para lidar com o fluxo do OAuth 2.0 em aplicações Web, com suporte a TypeScript. Atualmente o fluxo suportado é o `Implicit`, abaixo temos um exemplo de como configurá-lo. Este fluxo é usado para aplicativos móveis e aplicações web, onde a capacidade de armazenar segredos no cliente não é garantida. Entenda melhor sobre este fluxo neste guia [Introdução ao OAuth 2 da Digital Ocean](https://www.digitalocean.com/community/tutorials/uma-introducao-ao-oauth-2-pt) e caso queira entender na prática acesse o [OAuth 2.0 Playground](https://www.oauth.com/playground). - [Configurações](#configuracoes) - [Configurando variáveis dos Serviços](#configurando-variaveis) - [Configurando provedor OAuth](#configurando-provedor) - [Configurando Cliente (aplicação)](#configurando-cliente) - [Criando instância do OAuth](#criando-instancia-oauth) - [Adequando inicialização da aplicação](#adequando-inicializacao-aplicacao) - [Lidando com redirecionamento do Login](#lidando-redirecionamento-login) - [Obtendo token de acesso em segundo plano](#obtendo-novo-token-segundo-plano) - [Monitorando eventos de sessão](#monitorando-eventos-sessao) - [Fornecendo feedback ao usuário](#monitorando-eventos-sessao) - [Possibilitando Logout da Conta do Usuário](#possibilitando-logout) - [Requisições](#requisicoes) - [Autenticando requisições](#autenticando-requisicoes) - [Lidando com requisições que falharam](#lidando-com-requisicoes-que-falharam) - [Playground](#playground) ## Configurações <a name="configuracoes"></a> Após instalar, algumas configurações são necessárias. ### Configurando variáveis dos Serviços <a name="configurando-variaveis"></a> Para realizar as configurações é necessário saber os hosts de alguns serviços, como OAuth, Login e Usuários. Estes valores podem ser obtidos conforme constam em https://suite.cloud.betha.com.br/env.js Entretanto sugerimos a importação dessa URL como um script no `index.html` para que a aplicação possa obter os valores de forma dinâmica por meio da variável `window['___bth'].envs`. Assim caso em algum momento seja necessário alteração nestes valores, eles serão refletidos automaticamente. ```html <!DOCTYPE html> <html> <head> <title>OAuth Playground</title> </head> <body> <!-- ... HTML ... --> <!-- Variáveis do Env.js --> <script src="https://suite.cloud.betha.com.br/env.js"></script> <!-- App Scripts --> </body> </html> ``` > No [**playground/webpack.config.js**](./playground/webpack.config.js) é definido uma variável dinamica chamada "envjs" a qual irá preencher a URL nos templates [**playground/src/index.html**](./playground/src/index.html) e [**playground/src/auth/callback/callback.html**](./playground/src/auth/callback/callback.html) ### Configurando provedor OAuth <a name="configurando-provedor"></a> Com as variáveis dos serviços em mãos, basta criar uma configuração de provedor OAuth. ```js const OAUTH_URL = window["___bth"].envs.suite.oauth.v1.host; const SERVICE_LOGIN_URL = window["___bth"].envs.suite["service-login"].v1.host; const USERS_URL = window["___bth"].envs.suite.usuarios.v1.host; const provider = { authorization_endpoint: `${OAUTH_URL}/authorize`, check_session_iframe: `${SERVICE_LOGIN_URL}/openidsso.jsp`, end_session_endpoint: `${SERVICE_LOGIN_URL}/logout?continue=${OAUTH_URL}/authorize?client_id=${config.clientId}%26response_type=token%26redirect_uri=${config.redirectUri}%26scope=${config.scope}`, introspect_endpoint: `${OAUTH_URL}/tokeninfo`, token_endpoint: `${OAUTH_URL}/token`, userinfo_endpoint: `${USERS_URL}/api/usuarios/@me`, }; ``` > Exemplo em [**playground/src/auth/oauth-provider.js**](./playground/src/auth/oauth-provider.js) ### Configurando Cliente (aplicação) <a name="configurando-cliente"></a> Além do provedor também é preciso configurar o cliente, que representa a aplicação que irá consumir os recursos autenticados. _ℹ️ Atualmente as aplicações clientes são registradas e mantidas pela equipe da Plataforma, caso não possua os dados necessários, favor entrar em contato._ ```js const clientConfig = { scope: "SCOPES", clientId: "CLIENT_ID", redirectUri: `${window.location.origin}/auth/callback.html`, silentRedirectUri: `${window.location.origin}/auth/silent-callback.html`, }; ``` Onde: - `scope` e `clientId` são obtidos ao registrar uma aplicação - `redirectUri` e `silentRedirectUri` são os endereços de redirecionamento, que serão abordados mais a frente, nas seções [Lidando com redirecionamento do Login](#lidando-redirecionamento-login) e [Obtendo novo token de acesso em segundo plano](#obtendo-novo-token-segundo-plano), respectivamente. > Exemplo em [**playground/src/auth/oauth-application.js**](./playground/src/auth/oauth-application.js) ## Criando instância do OAuth <a name="criando-instancia-oauth"></a> As configurações do cliente e do provedor OAuth podem ser mescladas e utilizadas para criar uma instância do OAuth para a aplicação. É por meio desta instância que serão realizadas as interações com recursos de autenticação como: login, logout, obter token de acesso, verificar se há sessão ativa, obter dados do usuário, etc. ```js const OAUTH_URL = window["___bth"].envs.suite.oauth.v1.host; const SERVICE_LOGIN_URL = window["___bth"].envs.suite["service-login"].v1.host; const USERS_URL = window["___bth"].envs.suite.usuarios.v1.host; const oAuthConfig = { scope: "SCOPES", clientId: "CLIENT_ID", redirectUri: `${window.location.origin}/auth/callback.html`, silentRedirectUri: `${window.location.origin}/auth/silent-callback.html`, provider: { authorization_endpoint: `${OAUTH_URL}/authorize`, check_session_iframe: `${SERVICE_LOGIN_URL}/openidsso.jsp`, end_session_endpoint: `${SERVICE_LOGIN_URL}/logout?continue=${OAUTH_URL}/authorize?client_id=${config.clientId}%26response_type=token%26redirect_uri=${config.redirectUri}%26scope=${config.scope}`, introspect_endpoint: `${OAUTH_URL}/tokeninfo`, token_endpoint: `${OAUTH_URL}/token`, userinfo_endpoint: `${USERS_URL}/api/usuarios/@me`, }, }; export const oAuthApp: OAuthApplication = new OAuthApplication(oAuthConfig); ``` > Exemplo em [**playground/src/auth/oauth-application.js**](./playground/src/auth/oauth-application.js) ## Adequando inicialização da aplicação <a name="adequando-inicializacao-aplicacao"></a> Quando a aplicação for inicializada e não houver uma sessão ativa, deve-se chamar o método `login()`, disponível na instância do OAuth. Este método irá lidar com os passos necessários para o usuário efetuar o login. ```js import { oAuthApp } from "./oauth-application.ts"; if (!oAuthApp.hasActiveSession()) { oAuthApp.login(); } else { /** * Iniciar a aplicação, renderizando recursos autenticados */ bootstrap(); } ``` > Exemplo em [**playground/src/index.js**](./playground/src/index.js) ## Lidando com redirecionamento do Login <a name="lidando-redirecionamento-login"></a> Após o usuário efetuar o login, ele será redirecionado para o `redirectUri` configurado na instância do OAuth. Este redirecionamento irá entregar para a aplicação alguns valores por meio de parâmetros da URL. Nesta página a aplicação deverá executar o método `handleCallback()` da instância do OAuth para prosseguir com o fluxo. ```js import { oAuthApp } from "./oauth-application.ts"; oAuthApp.handleCallback(); ``` > Exemplo em [**playground/src/auth/callback/callback.html**](./playground/src/auth/callback/callback.html) e [**playground/src/auth/callback/callback.js**](./playground/src/auth/callback/callback.js) ## Obtendo novo token de acesso em segundo plano <a name="obtendo-novo-token-segundo-plano"></a> Após autenticado, é possível renovar o token de acesso em segundo plano. Para isso é necessário ter uma página que emita algumas informações para a aplicação de origem. Essa página é configurada no `silentRedirectUri` ao instanciar a aplicação OAuth. ```html <html> <head> <script> parent.postMessage(location.hash, location.origin); </script> </head> </html> ``` > Exemplo em [**playground/src/auth/callback/silent-callback.html**](./playground/src/auth/callback/silent-callback.html) ## Monitorando eventos de sessão <a name="monitorando-eventos-sessao"></a> Durante o ciclo de vida de uma sessão alguns eventos provenientes de autenticação podem ser capturados para apresentar um feedback ao usuário. A captura dos eventos pode ser feita por meio da instância de um Monitor de Eventos, criado a partir da instância do OAuth. ```js import { OAuthMonitor } from "@betha-plataforma/oauth"; import { oauthApp } from "./oauth-application.ts"; const monitorOptions = { app: oauthApp, interval: 1000, }; const monitor = new OAuthMonitor(monitorOptions, { onSessionChanged: () => { // OAuth session changed }, onSessionEnded: () => { // OAuth session ended }, onSessionRestablished: () => { // OAuth session restabilished }, }); monitor.start(); ``` > Exemplo em [**playground/src/auth/oauth-monitor.js**](./playground/src/auth/oauth-monitor.js) ### Fornecendo feedback ao usuário O componente recomendado para o feedback é a **modal**. Ela deve ser utilizada no formato bloqueante, ou seja, não permite fechar por meio da interface ou do teclado. Isto evita que o usuário utilize o sistema que pode estar inoperável por falta de autenticação. Para evitar empilhamento, é sugerido fechar as modais abertas ao receber qualquer evento. As interfaces apresentadas são modelos baseados no Design System dos sistemas Cloud da Betha. É importante preservar as características do sistema no qual as interfaces serão apresentadas. _ℹ️ Nos exemplos abaixo foi utilizado o [**Bootstrap 4**](https://github.com/twbs/bootstrap), que é um framework bem comum para abstrair o comportamento e estilização dos componentes._ #### Quando a sessão for alterada Abaixo um modelo de apresentação quando a sessão for alterada ![modal_session_changed](./docs/images/modal_session_changed.png) - Ilustração: [user-changed.png](./playground/src/images/user-changed.png) - Mensagem: "O usuário **lorem.ipsum** entrou no sistema" - Botão: "Atualizar página" ```html <div id="session_changed_modal" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-hidden="true" > <div class="modal-dialog"> <div class="modal-content"> <div class="modal-body p-4"> <div class="row text-center"> <div class="col-md-12"> <img style="width: 110px; margin-bottom: 12px;" src="/images/user-changed.png" /> <h5 class="mb-4"> O usuário <strong id="session_changed_userid"></strong> entrou no sistema </h5> </div> </div> <div class="text-center"> <button class="btn btn-primary" onclick="window.location.reload()" style="width: 220px;" > Atualizar página </button> </div> </div> </div> </div> </div> ``` O evento pode ser capturado por meio do método `onSessionChanged`, disponível no Monitor de Eventos ```js new OAuthMonitor(monitorOptions, { onSessionChanged: () => { const userInfo = getUserInfo(); document.querySelector("#session_changed_userid").innerHTML = userInfo.id; $("#session_ended_modal").modal("hide"); $("#session_changed_modal").modal("show"); }, }); ``` > Exemplo em [**playground/src/index.html**](./playground/src/index.html) e [**playground/src/auth/oauth-monitor.js**](./playground/src/auth/oauth-monitor.js) #### Quando a sessão for encerrada Abaixo um modelo de apresentação quando a sessão for encerrada ![modal_session_ended](./docs/images/modal_session_ended.png) - Ilustração: [logout.png](./playground/src/images/logout.png) - Mensagem: "Você saiu do sistema" - Botão: "Fazer login" ```html <div id="session_ended_modal" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-hidden="true" > <div class="modal-dialog"> <div class="modal-content"> <div class="modal-body p-4"> <div class="row text-center"> <div class="col-md-12"> <img style="width: 110px; margin-bottom: 12px;" src="/images/logout.png" /> <h5 class="mb-4">Você saiu do sistema</h5> </div> </div> <div class="text-center"> <button class="btn btn-primary" onclick="window.location.reload()" style="width: 220px;" > Fazer login </button> </div> </div> </div> </div> </div> ``` O evento pode ser capturado por meio do método `onSessionEnded`, disponível no Monitor de Eventos ```js new OAuthMonitor(monitorOptions, { onSessionEnded: () => { $("#session_changed_modal").modal("hide"); $("#session_ended_modal").modal("show"); }, }); ``` > Exemplo em [**playground/src/index.html**](./playground/src/index.html) e [**playground/src/auth/oauth-monitor.js**](./playground/src/auth/oauth-monitor.js) ## Possibilitando Logout da Conta do Usuário <a name="possibilitando-logout"></a> Esta parte da interface compõe a estrutura visual da aplicação e deve ser utilizada para fornecer as informações da sessão atual como nome, usuário e foto e também possibilita a realização do _logout_ e acesso à Central do Usuário. ![./docs/images/conta-usuario.png](./docs/images/conta-usuario.png) _ℹ️ Neste exemplo foi utilizado os Web Components da biblioteca [@betha-plataforma/estrutura-componentes](https://github.com/betha-plataforma/estrutura-componentes), que fornece os componentes necessários para compor a estrutura de uma aplicação front-end de maneira agnóstica a frameworks_ Criando o componente na interface ```html <html> <head> <link rel="stylesheet" href="https://unpkg.com/@betha-plataforma/estrutura-componentes/dist/estrutura-componentes/estrutura-componentes.css" /> </head> <body> <bth-app menu-vertical> <bth-conta-usuario slot="menu_ferramentas"></bth-conta-usuario> </bth-app> <script type="module" src="https://unpkg.com/@betha-plataforma/estrutura-componentes/dist/estrutura-componentes/estrutura-componentes.esm.js" ></script> <script nomodule src="https://unpkg.com/@betha-plataforma/estrutura-componentes/dist/estrutura-componentes/estrutura-componentes.js" ></script> </body> </html> ``` Configurando informações da sessão e método de logout ```js import { oAuthApp } from "./oauth-application"; const profile = oAuthApp.getUser(); const accessToken = oAuthApp.getSession().accessToken.access_token; const contaUsuario = document.querySelector("bth-conta-usuario"); contaUsuario.usuario = profile.id; contaUsuario.nome = profile.name; contaUsuario.fotoUrl = `${profile.photo}?access_token=${accessToken}`; contaUsuario.addEventListener("logout", async () => { await oAuthApp.logout(); }); ``` > Exemplo em [**playground/src/index.html**](./playground/src/index.html), [**playground/src/app/bootstrap.js**](./playground/src/app/bootstrap.js) e [**playground/src/auth/services/authentication.js**](./playground/src/auth/services/authentication.js) ## Requisições <a name="requisicoes"></a> Ao interagir com recursos autenticados por meio de requisições HTTP, algumas operações podem ser padronizadas no intuito de abstrair a necessidade de lidar com autenticação em cada funcionalidade. Geralmente cria-se um _Client HTTP_ responsável por implementar este mecanismo. *ℹ️ Neste exemplo foi utilizado o [**Axios**](https://github.com/axios/axios), que é um HTTP Client bem comum para *Browsers*.* ### Autenticando requisições <a name="autenticando-requisicoes"></a> O cabeçalho `Authorization` deve estar presente, com o valor `Bearer <AUTH_TOKEN>`, onde o `AUTH_TOKEN` pode ser obtido da instância do OAuth. ```js import { oAuthApp } from './oauth-application'; const Axios = axios.create(); /** * Registra interceptor para autenticar requisições */ Axios.interceptors.request.use(config => { if (config.method === 'OPTIONS') { return config; } const accessToken = oAuthApp.getSession().accessToken.access_token; config.headers.Authorization = `Bearer ${accessToken}`; return config; }); export Axios; ``` > Exemplo em [**playground/src/core/api.js**](./playground/src/core/api.js) e [**playground/src/auth/services/authentication-context.js**](./playground/src/auth/services/authentication-context.js) ### Lidando com requisições que falharam <a name="lidando-com-requisicoes-que-falharam"></a> As requisições que falharam com o código de resposta `401 (Unauthorized)` devem ser armazenadas. Na sequência o processo de atualização do token da sessão deve ser realizado: - Caso seja possível atualizar, as requisições que falharam devem ser efetuadas novamente. - Caso não seja possível atualizar, deve ser chamado o método de `login` ```js import { oAuthApp } from "./oauth-application"; import { addRequestRetry } from "./retries.js"; const Axios = axios.create(); /** * Registra interceptor para lidar com requisições que falharam */ Axios.interceptors.response.use( (response) => response, (error) => { if (error.response.status === 401) { return new Promise((resolve, reject) => addRequestRetry(error.config, resolve, reject) ); } return Promise.reject(error); } ); export default Axios; ``` O interceptor armazena as requisições que falharam para possibilitar o mecanismo de retentativa. Caso não consiga atualizar o token, é solicitado o login. ```js import api from "./api"; import { oAuthApp } from "./oauth-application"; let isRefreshingToken = false; let retryQueue = []; async function requireAuthentication() { try { await oAuthApp.silentRefresh(); } catch (e) { return oAuthApp.login(); } const session = oAuthApp.getSession(); return session.accessToken.access_token; } export function addRequestRetry(request, resolve, reject) { retryQueue.push({ request, resolve, reject }); if (!isRefreshingToken) { startRefreshing(); return requireAuthentication() .then(() => { retryAllRequests(); stopRefreshing(); }) .catch(stopRefreshing); } } function retryAllRequests() { retryQueue.forEach(retryRequest); retryQueue = []; } function retryRequest({ request, resolve, reject }) { api.request(request).then(resolve).then(reject); } function startRefreshing() { isRefreshingToken = true; } function stopRefreshing() { isRefreshingToken = false; } ``` > Exemplo em [**playground/src/core/api.js**](./playground/src/core/api.js), [**playground/src/core/retries.js**](./playground/src/services/retries.js) e [**playground/src/auth/services/authentication.js**](./playground/src/auth/services/authentication.js) ## Playground Exemplos podem ser encontrados no [**playground**](./playground/README.md)