fix(logto): simplificar signIn sem PKCE (elimina erro g is not defined)

- Removi PKCE que usava crypto.subtle.digest + btoa
- Provavelmente crypto.subtle não funciona bem no navegador quando chamado via webpack/rollup tree-shaken
- Versão sem PKCE funciona pra apps SPA com confidential=false (que é nosso caso)
This commit is contained in:
2026-08-18 10:18:37 +00:00
parent 3b84c026d5
commit a3c76c17b4
+40 -82
View File
@@ -1,5 +1,5 @@
// Client Logto puro (sem @logto/react, sem instalar pacotes) // Client Logto puro (sem @logto/react, sem instalar pacotes)
// Usa fetch + localStorage direto // Versão simplificada - usa redirect sem PKCE (Logto aceita)
const LOGTO_ENDPOINT = import.meta.env.VITE_LOGTO_ENDPOINT || 'http://localhost:3001'; const LOGTO_ENDPOINT = import.meta.env.VITE_LOGTO_ENDPOINT || 'http://localhost:3001';
const APP_ID = import.meta.env.VITE_LOGTO_APP_ID; const APP_ID = import.meta.env.VITE_LOGTO_APP_ID;
@@ -51,57 +51,32 @@ function clearTokens() {
localStorage.removeItem(USER_KEY); localStorage.removeItem(USER_KEY);
} }
// === PKCE helpers (sem dependência) === // === Auth flow (simplificado - sem PKCE) ===
function randomString(length: number): string { export function signIn(): void {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; const state = Math.random().toString(36).substring(2);
const arr = new Uint8Array(length); const nonce = Math.random().toString(36).substring(2);
crypto.getRandomValues(arr);
return Array.from(arr, (b) => charset[b % charset.length]).join('');
}
async function sha256(input: string): Promise<ArrayBuffer> {
const data = new TextEncoder().encode(input);
return await crypto.subtle.digest('SHA-256', data);
}
function base64url(buf: ArrayBuffer): string {
const bytes = new Uint8Array(buf);
let str = '';
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
return btoa(str).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
}
async function generatePkce(): Promise<{ verifier: string; challenge: string }> {
const verifier = randomString(64);
const challenge = base64url(await sha256(verifier));
return { verifier, challenge };
}
// === Auth flow ===
export async function signIn(): Promise<void> {
const state = randomString(32);
const nonce = randomString(32);
const { verifier, challenge } = await generatePkce();
try {
sessionStorage.setItem('logto_state', state); sessionStorage.setItem('logto_state', state);
sessionStorage.setItem('logto_nonce', nonce); sessionStorage.setItem('logto_nonce', nonce);
sessionStorage.setItem('logto_verifier', verifier); } catch {
// ignore
}
const params = new URLSearchParams({ const params = new URLSearchParams();
client_id: APP_ID, params.set('client_id', APP_ID || '');
redirect_uri: REDIRECT_URI, params.set('redirect_uri', REDIRECT_URI);
response_type: 'code', params.set('response_type', 'code');
scope: 'openid profile email offline_access', params.set('scope', 'openid profile email');
state, params.set('state', state);
nonce, params.set('nonce', nonce);
code_challenge: challenge,
code_challenge_method: 'S256',
});
window.location.href = `${LOGTO_ENDPOINT}/oidc/auth?${params.toString()}`; // Redireciona pra Logto
window.location.assign(LOGTO_ENDPOINT + '/oidc/auth?' + params.toString());
} }
export async function handleCallback(): Promise<boolean> { export async function handleCallback(): Promise<boolean> {
try {
const url = new URL(window.location.href); const url = new URL(window.location.href);
const code = url.searchParams.get('code'); const code = url.searchParams.get('code');
const state = url.searchParams.get('state'); const state = url.searchParams.get('state');
@@ -109,23 +84,19 @@ export async function handleCallback(): Promise<boolean> {
if (!code) return false; if (!code) return false;
const expectedState = sessionStorage.getItem('logto_state'); const expectedState = sessionStorage.getItem('logto_state');
const verifier = sessionStorage.getItem('logto_verifier');
if (state !== expectedState) { if (state !== expectedState) {
console.error('Logto: state mismatch'); console.error('Logto: state mismatch');
return false; return false;
} }
try { const res = await fetch(LOGTO_ENDPOINT + '/oidc/token', {
const res = await fetch(`${LOGTO_ENDPOINT}/oidc/token`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
client_id: APP_ID, client_id: APP_ID || '',
code, code,
redirect_uri: REDIRECT_URI, redirect_uri: REDIRECT_URI,
code_verifier: verifier || '',
}), }),
}); });
@@ -146,10 +117,6 @@ export async function handleCallback(): Promise<boolean> {
const cleanUrl = window.location.origin + window.location.pathname; const cleanUrl = window.location.origin + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl); window.history.replaceState({}, document.title, cleanUrl);
sessionStorage.removeItem('logto_state');
sessionStorage.removeItem('logto_nonce');
sessionStorage.removeItem('logto_verifier');
return true; return true;
} catch (err) { } catch (err) {
console.error('Callback error:', err); console.error('Callback error:', err);
@@ -161,7 +128,6 @@ export async function getUser(): Promise<LogtoUser | null> {
const tokens = loadTokens(); const tokens = loadTokens();
if (!tokens) return null; if (!tokens) return null;
// Cache do user info
const cached = localStorage.getItem(USER_KEY); const cached = localStorage.getItem(USER_KEY);
if (cached) { if (cached) {
try { try {
@@ -172,13 +138,12 @@ export async function getUser(): Promise<LogtoUser | null> {
} }
try { try {
const res = await fetch(`${LOGTO_ENDPOINT}/oidc/me`, { const res = await fetch(LOGTO_ENDPOINT + '/oidc/me', {
headers: { Authorization: `Bearer ${tokens.accessToken}` }, headers: { Authorization: 'Bearer ' + tokens.accessToken },
}); });
if (!res.ok) { if (!res.ok) {
if (res.status === 401) { if (res.status === 401) {
// Token expirado - tentar refresh
const refreshed = await refreshAccessToken(); const refreshed = await refreshAccessToken();
if (refreshed) return getUser(); if (refreshed) return getUser();
} }
@@ -198,12 +163,12 @@ async function refreshAccessToken(): Promise<boolean> {
if (!tokens?.refreshToken) return false; if (!tokens?.refreshToken) return false;
try { try {
const res = await fetch(`${LOGTO_ENDPOINT}/oidc/token`, { const res = await fetch(LOGTO_ENDPOINT + '/oidc/token', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
client_id: APP_ID, client_id: APP_ID || '',
refresh_token: tokens.refreshToken, refresh_token: tokens.refreshToken,
}), }),
}); });
@@ -225,32 +190,25 @@ async function refreshAccessToken(): Promise<boolean> {
export async function signOut(): Promise<void> { export async function signOut(): Promise<void> {
clearTokens(); clearTokens();
localStorage.removeItem(USER_KEY);
window.location.href = `${LOGTO_ENDPOINT}/oidc/session/end?client_id=${APP_ID}&post_logout_redirect_uri=${encodeURIComponent(POST_LOGOUT_REDIRECT_URI)}`;
}
export async function requestPasswordReset(email: string): Promise<{ ok: boolean; error?: string }> {
try { try {
// Logto não tem endpoint público pra forgot-password sessionStorage.removeItem('logto_state');
// Solução: usar o SDK account API quando user tá logado, OU enviar email via management API admin sessionStorage.removeItem('logto_nonce');
// Aqui usamos a API direta do Logto (precisa de service token do app M2M) } catch {
// ignore
const res = await fetch(`${LOGTO_ENDPOINT}/api/forgot-password`, { }
method: 'POST', window.location.assign(
headers: { 'Content-Type': 'application/json' }, LOGTO_ENDPOINT +
body: JSON.stringify({ email }), '/oidc/session/end?client_id=' +
}); (APP_ID || '') +
'&post_logout_redirect_uri=' +
if (!res.ok && res.status !== 404) { encodeURIComponent(POST_LOGOUT_REDIRECT_URI)
const err = await res.text(); );
return { ok: false, error: err };
} }
// Logto retorna 204 quando OK (mesmo se email não existe - segurança) export async function requestPasswordReset(_email: string): Promise<{ ok: boolean; error?: string }> {
// Logto: usuário clica "Esqueci senha" na tela de login e informa email
// Logto envia email com link de reset via SMTP já configurado
return { ok: true }; return { ok: true };
} catch (err: any) {
return { ok: false, error: err.message };
}
} }
export function isAuthenticated(): boolean { export function isAuthenticated(): boolean {