Biblioteca JavaScript para lidar com o fluxo do OAuth 2.0 em aplicações Web, com suporte a TypeScript.
npm install @betha-plataforma/oauthBiblioteca 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 e caso queira entender na prática acesse o OAuth 2.0 Playground.
- Configurações
- Configurando variáveis dos Serviços
- Configurando provedor OAuth
- Configurando Cliente (aplicação)
- Criando instância do OAuth
- Adequando inicialização da aplicação
- Lidando com redirecionamento do Login
- Obtendo token de acesso em segundo plano
- Monitorando eventos de sessão
- Fornecendo feedback ao usuário
- Possibilitando Logout da Conta do Usuário
- Requisições
- Autenticando requisições
- Lidando com requisições que falharam
- Playground
Após instalar, algumas configurações são necessárias.
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
`
> No playground/webpack.config.js é definido uma variável dinamica chamada "envjs" a qual irá preencher a URL nos templates playground/src/index.html e playground/src/auth/callback/callback.html
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,${SERVICE_LOGIN_URL}/openidsso.jsp
check_session_iframe: ,${SERVICE_LOGIN_URL}/logout?continue=${OAUTH_URL}/authorize?client_id=${config.clientId}%26response_type=token%26redirect_uri=${config.redirectUri}%26scope=${config.scope}
end_session_endpoint: ,${OAUTH_URL}/tokeninfo
introspect_endpoint: ,${OAUTH_URL}/token
token_endpoint: ,${USERS_URL}/api/usuarios/@me
userinfo_endpoint: ,`
};
> Exemplo em playground/src/auth/oauth-provider.js
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${window.location.origin}/auth/callback.html
const clientConfig = {
scope: "SCOPES",
clientId: "CLIENT_ID",
redirectUri: ,${window.location.origin}/auth/silent-callback.html
silentRedirectUri: ,`
};
Onde:
- scope e clientId são obtidos ao registrar uma aplicaçãoredirectUri
- e silentRedirectUri são os endereços de redirecionamento, que serão abordados mais a frente, nas seções Lidando com redirecionamento do Login e Obtendo novo token de acesso em segundo plano, respectivamente.
> Exemplo em playground/src/auth/oauth-application.js
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,${window.location.origin}/auth/silent-callback.html
silentRedirectUri: ,${OAUTH_URL}/authorize
provider: {
authorization_endpoint: ,${SERVICE_LOGIN_URL}/openidsso.jsp
check_session_iframe: ,${SERVICE_LOGIN_URL}/logout?continue=${OAUTH_URL}/authorize?client_id=${config.clientId}%26response_type=token%26redirect_uri=${config.redirectUri}%26scope=${config.scope}
end_session_endpoint: ,${OAUTH_URL}/tokeninfo
introspect_endpoint: ,${OAUTH_URL}/token
token_endpoint: ,${USERS_URL}/api/usuarios/@me
userinfo_endpoint: ,
},
};
export const oAuthApp: OAuthApplication = new OAuthApplication(oAuthConfig);
`
> Exemplo em playground/src/auth/oauth-application.js
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
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 e playground/src/auth/callback/callback.js
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`
> Exemplo em playground/src/auth/callback/silent-callback.html
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
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, 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
- Ilustração: user-changed.png
- Mensagem: "O usuário lorem.ipsum entrou no sistema"
- Botão: "Atualizar página"
`html
id="session_changed_modal"
class="modal fade"
data-backdrop="static"
data-keyboard="false"
tabindex="-1"
aria-hidden="true"
>
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 e playground/src/auth/oauth-monitor.js
#### Quando a sessão for encerrada
Abaixo um modelo de apresentação quando a sessão for encerrada
- Ilustração: logout.png
- Mensagem: "Você saiu do sistema"
- Botão: "Fazer login"
`html
id="session_ended_modal"
class="modal fade"
data-backdrop="static"
data-keyboard="false"
tabindex="-1"
aria-hidden="true"
>
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 e playground/src/auth/oauth-monitor.js
Possibilitando Logout da Conta do Usuário
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
_ℹ️ Neste exemplo foi utilizado os Web Components da biblioteca @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
rel="stylesheet"
href="https://unpkg.com/@betha-plataforma/estrutura-componentes/dist/estrutura-componentes/estrutura-componentes.css"
/>
type="module"
src="https://unpkg.com/@betha-plataforma/estrutura-componentes/dist/estrutura-componentes/estrutura-componentes.esm.js"
>
nomodule
src="https://unpkg.com/@betha-plataforma/estrutura-componentes/dist/estrutura-componentes/estrutura-componentes.js"
>
`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/app/bootstrap.js e playground/src/auth/services/authentication.js
Requisições
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, que é um HTTP Client bem comum para Browsers.
$3
O cabeçalho
Authorization deve estar presente, com o valor Bearer , 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 e playground/src/auth/services/authentication-context.js
$3
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/retries.js e playground/src/auth/services/authentication.js
Exemplos podem ser encontrados no playground