Pular para o conteúdo principal

Adicionar autenticação ao seu aplicativo web tradicional

nota

Este guia assume que você criou um Aplicativo do tipo "Traditional web" no Console de Administração.

Seu aplicativo pode rodar no lado do servidor em vez do navegador, usando frameworks como Django, Laravel, etc. Isso é chamado de aplicativo web tradicional. Se você não encontrar um SDK adequado nesta página, provavelmente precisará integrar manualmente.

Este artigo orienta você sobre como concluir isso passo a passo. E tomamos o Express no Node.js como exemplo.

dica

Este artigo não é apenas para Express, e se você estiver usando outros frameworks ou até mesmo outras linguagens, pode substituir @logto/js pelo SDK principal da outra linguagem e, em seguida, ajustar algumas das etapas.

Obter código-fonte

Você pode ir ao GitHub para obter o código final deste guia.

Iniciar um projeto Express

Com express-generator, você pode iniciar rapidamente um projeto Express.

mkdir express-logto
cd express-logto
npx express-generator

Instalar dependências

O aplicativo de demonstração precisará de 4 dependências:

  1. @logto/js: SDK principal do Logto para JavaScript.
  2. node-fetch: Código mínimo para uma API compatível com window.fetch no runtime do Node.js.
  3. express-session: Um middleware de sessão, usaremos a sessão para armazenar tokens de usuário.
  4. js-base64: Mais um transcodificador Base64.
npm i @logto/js node-fetch@v2 express-session js-base64

Usar sessão

Quando os usuários fazem login, eles receberão um conjunto de tokens (Tokens de acesso, Tokens de ID, Tokens de atualização) e dados de interação, e a sessão é um excelente lugar para armazená-los.

Instalamos express-session na etapa anterior, então agora vamos simplesmente adicionar o seguinte código para configurá-lo:

// app.js

const session = require('express-session');

app.use(
session({
secret: 'keyboard cat', // Altere para sua própria chave secreta
cookie: { maxAge: 86400 },
})
);

Implementar funções para autenticar usuários

dica

Assumimos que o aplicativo está rodando em http://localhost:3000 nos trechos de código a seguir.

Nesta etapa, precisamos implementar as seguintes funções de autenticação:

  1. getSignInUrl: constrói e retorna uma URL completa do Servidor de Autorização Logto para a qual os usuários serão redirecionados.
  2. handleSignIn: analisa a URL de callback após o processo de autenticação ser concluído, obtém o parâmetro de consulta de código e, em seguida, busca tokens (um token de acesso, o token de atualização e um token de ID) para concluir o processo de login.
  3. refreshTokens: troca um novo token de acesso usando o token de atualização.
dica

Você pode encontrar e copiar o "App Secret" na página de detalhes do aplicativo no Admin Console:

App Secret
// logto.js

const {
withReservedScopes,
fetchOidcConfig,
discoveryPath,
generateSignInUri,
verifyAndParseCodeFromCallbackUri,
fetchTokenByAuthorizationCode,
fetchTokenByRefreshToken,
} = require('@logto/js');
const fetch = require('node-fetch');
const { randomFillSync, createHash } = require('crypto');
const { fromUint8Array } = require('js-base64');

const config = {
endpoint: 'https://logto.dev',
appId: 'foo',
appSecret: '<your-app-secret-copied-from-console>',
redirectUri: 'http://localhost:3000/callback', // Você pode precisar substituí-lo pelo endereço de produção do seu aplicativo
scopes: withReservedScopes().split(' '),
};

const requester = (input, init) => {
const { appId, appSecret } = config;

return fetch(input, {
...init,
headers: {
...init?.headers,
authorization: `Basic ${Buffer.from(`${appId}:${appSecret}`, 'utf8').toString('base64')}`,
},
});
};

const generateRandomString = (length = 64) => {
return fromUint8Array(randomFillSync(new Uint8Array(length)), true);
};

const generateCodeChallenge = async (codeVerifier) => {
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
const hash = createHash('sha256');
hash.update(encodedCodeVerifier);
const codeChallenge = hash.digest();
return fromUint8Array(codeChallenge, true);
};

const getOidcConfig = async () => {
return fetchOidcConfig(new URL(discoveryPath, config.endpoint).toString(), requester);
};

exports.getSignInUrl = async () => {
const { authorizationEndpoint } = await getOidcConfig();
const codeVerifier = generateRandomString();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateRandomString();

const { redirectUri, scopes, appId: clientId } = config;

const signInUri = generateSignInUri({
authorizationEndpoint,
clientId,
redirectUri: redirectUri,
codeChallenge,
state,
scopes,
});

return { redirectUri, codeVerifier, state, signInUri };
};

exports.handleSignIn = async (signInSession, callbackUri) => {
const { redirectUri, state, codeVerifier } = signInSession;
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);

const { appId: clientId } = config;
const { tokenEndpoint } = await getOidcConfig();
const codeTokenResponse = await fetchTokenByAuthorizationCode(
{
clientId,
tokenEndpoint,
redirectUri,
codeVerifier,
code,
},
requester
);

return codeTokenResponse;
};

exports.refreshTokens = async (refreshToken) => {
const { appId: clientId, scopes } = config;
const { tokenEndpoint } = await getOidcConfig();
const tokenResponse = await fetchTokenByRefreshToken(
{
clientId,
tokenEndpoint,
refreshToken,
scopes,
},
requester
);

return tokenResponse;
};

Fazer login

Antes de mergulharmos nos detalhes, aqui está uma visão geral rápida da experiência do usuário final. O processo de login pode ser simplificado da seguinte forma:

  1. Seu aplicativo invoca o método de login.
  2. O usuário é redirecionado para a página de login do Logto. Para aplicativos nativos, o navegador do sistema é aberto.
  3. O usuário faz login e é redirecionado de volta para o seu aplicativo (configurado como o URI de redirecionamento).
Sobre o login baseado em redirecionamento
  1. Este processo de autenticação segue o protocolo OpenID Connect (OIDC), e o Logto aplica medidas de segurança rigorosas para proteger o login do usuário.
  2. Se você tiver vários aplicativos, pode usar o mesmo provedor de identidade (Logto). Uma vez que o usuário faz login em um aplicativo, o Logto completará automaticamente o processo de login quando o usuário acessar outro aplicativo.

Para saber mais sobre a lógica e os benefícios do login baseado em redirecionamento, veja Experiência de login do Logto explicada.


Configurar Redirect URI

Vamos mudar para a página de detalhes do Aplicativo no Logto Console. Adicione um URI de redirecionamento http://localhost:3000/callback e clique em "Salvar alterações".

URI de redirecionamento no Logto Console

Implementar rota de login

nota

Antes de chamar getSignInUrl(), certifique-se de que você configurou corretamente o URI de redirecionamento no Admin Console.

Crie uma rota no Express para fazer login:

const { getSignInUrl } = require('./logto');

app.get('/sign-in', async (req, res) => {
const { redirectUri, codeVerifier, state, signInUri } = await getSignInUrl();
req.session.signIn = { codeVerifier, state, redirectUri };
res.redirect(signInUri);
});

e uma rota para lidar com o callback:

app.get('/callback', async (req, res) => {
if (!req.session.signIn) {
res.send('Bad request.');
return;
}

const response = await handleSignIn(
req.session.signIn,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
req.session.signIn = null;

res.redirect('/');
});

Fazer logout

TODO: link para o capítulo "sessão & cookies" na referência de usuários.

Você pode limpar tokens na sessão para fazer logout de um usuário deste aplicativo. E confira este link para ler mais sobre "fazer logout".

app.get('/sign-out', (req, res) => {
req.session.tokens = null;
res.send('Sign out successful');
});

Acessar recurso protegido

Crie um middleware chamado withAuth para anexar um objeto auth ao req e verificar se um usuário está logado.

// auth.js

const { decodeIdToken } = require('@logto/js');
const { refreshTokens } = require('./logto');

const withAuth =
({ requireAuth } = { requireAuth: true }) =>
async (req, res, next) => {
if (requireAuth && !req.session.tokens) {
res.redirect('/sign-in');
return;
}

if (req.session.tokens) {
if (req.session.tokens.expiresAt >= Date.now()) {
// Token de acesso expirado, atualizar para novos tokens
try {
const response = await refreshTokens(req.session.tokens.refreshToken);
req.session.tokens = {
...response,
expiresAt: response.expiresIn + Date.now(),
idToken: decodeIdToken(response.idToken),
};
} catch {
// Falha na troca, redirecionar para login
res.redirect('/sign-in');
return;
}
}

req.auth = req.session.tokens.idToken.sub;
}

next();
};

module.exports = withAuth;

crie a página index, mostre um link de login para convidados e um link para o perfil para usuários que já fizeram login:

router.get('/', withAuth({ requireAuth: false }), function (req, res, next) {
res.render('index', { auth: Boolean(req.auth) });
});
extends layout

block content
h1 Hello logto
if auth
p: a(href="/user") Go to profile
else
p: a(href="/sign-in") Click here to sign in

crie a página user, para exibir userId (sujeito):

app.get('/user', withAuth(), (req, res, next) => {
res.render('user', { userId: req.auth });
});
extends layout

block content
h1 Hello logto
p userId: #{userId}

Leituras adicionais

Fluxos do usuário final: fluxos de autenticação, fluxos de conta e fluxos de organização Configurar conectores Proteger sua API