export interface TokenResponse { access_token: string refresh_token: string id_token: string expires_in: number refresh_expires_in?: number token_type: string scope?: string } function realmBase(): string { const cfg = useRuntimeConfig() return `${cfg.keycloakUrl}/realms/${cfg.keycloakRealm}` } export function buildAuthUrl(opts: { codeChallenge: string state: string redirectUri: string loginHint?: string }): string { const cfg = useRuntimeConfig() const params = new URLSearchParams({ client_id: cfg.keycloakClientId, redirect_uri: opts.redirectUri, response_type: 'code', scope: 'openid profile email', code_challenge: opts.codeChallenge, code_challenge_method: 'S256', state: opts.state, }) if (opts.loginHint) params.set('login_hint', opts.loginHint) return `${realmBase()}/protocol/openid-connect/auth?${params.toString()}` } export async function exchangeCodeForTokens(opts: { code: string codeVerifier: string redirectUri: string }): Promise { const cfg = useRuntimeConfig() const body = new URLSearchParams({ grant_type: 'authorization_code', code: opts.code, client_id: cfg.keycloakClientId, client_secret: cfg.keycloakClientSecret, redirect_uri: opts.redirectUri, code_verifier: opts.codeVerifier, }) return await $fetch( `${realmBase()}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), }, ) } export async function refreshTokens(refreshToken: string): Promise { const cfg = useRuntimeConfig() const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: cfg.keycloakClientId, client_secret: cfg.keycloakClientSecret, }) return await $fetch( `${realmBase()}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), }, ) } export function buildLogoutUrl(opts: { idTokenHint: string postLogoutRedirectUri: string }): string { const params = new URLSearchParams({ id_token_hint: opts.idTokenHint, post_logout_redirect_uri: opts.postLogoutRedirectUri, }) return `${realmBase()}/protocol/openid-connect/logout?${params.toString()}` } /** * Decodifica payload de JWT sem validar assinatura — uso server-only para * extrair claims do id_token/access_token recém-emitido pelo Keycloak. * NÃO USAR para validar tokens recebidos de terceiros. */ export function decodeJwtPayload(token: string): Record { const [, payload] = token.split('.') if (!payload) return {} try { const json = Buffer.from(payload, 'base64url').toString('utf-8') return JSON.parse(json) as Record } catch { return {} } }