Receita — Auth no frontend (SPA)
Cenário: você tem um app React/Next.js e quer autenticar usuários usando o Auth Platform, exibindo as informações do usuário logado e fazendo chamadas autenticadas a uma API.
O que você vai construir
Browser (sua SPA)
│
├─ 1. Clica "Entrar" → redirect para Auth Platform
│ │
│ └─ Usuário faz login (senha, MFA, Google)
│ │
│ ◄─ redirect /callback?code=...
│
├─ 2. Troca code por JWT (access_token + refresh_token)
├─ 3. Mostra perfil do usuário
└─ 4. Chama API com Bearer token
Pré-requisitos
- Auth Platform rodando em
http://localhost:4000 - Sistema registrado com o
clientIddo seu app (veja Início rápido)
npm install oidc-client-ts
Passo 1 — Configurar o UserManager
Crie um singleton que gerencia toda a sessão OIDC:
// src/lib/auth.ts
import { UserManager, WebStorageStateStore, type User } from 'oidc-client-ts';
const AUTH_URL = process.env.NEXT_PUBLIC_AUTH_URL ?? 'http://localhost:4000';
const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID ?? '';
let _mgr: UserManager | null = null;
export function getManager(): UserManager {
// Deve rodar apenas no browser
if (typeof window === 'undefined') throw new Error('client-side only');
if (!_mgr) {
_mgr = new UserManager({
authority: AUTH_URL,
client_id: CLIENT_ID,
redirect_uri: `${window.location.origin}/callback`,
post_logout_redirect_uri: `${window.location.origin}/`,
scope: 'openid email profile offline_access',
response_type: 'code', // Authorization Code
automaticSilentRenew: true, // renova antes de expirar
userStore: new WebStorageStateStore({
store: window.sessionStorage, // persiste na aba atual
}),
});
// Logar erros de renovação silenciosa
_mgr.events.addSilentRenewError((err) => {
console.error('[auth] silent renew failed:', err);
});
}
return _mgr;
}
// Ações
export const login = () => getManager().signinRedirect();
export const logout = () => getManager().signoutRedirect();
export const handleCallback = () => getManager().signinRedirectCallback();
export const getUser = () => getManager().getUser();
export const getToken = async (): Promise<string | null> => {
const u = await getManager().getUser();
return u && !u.expired ? u.access_token : null;
};
export type { User };
Passo 2 — Página de callback
Processa o retorno do Auth Platform após o login:
// src/app/callback/page.tsx (Next.js App Router)
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { handleCallback } from '@/lib/auth';
export default function CallbackPage() {
const router = useRouter();
useEffect(() => {
handleCallback()
.then(() => router.replace('/dashboard')) // redireciona após login
.catch((err) => {
console.error('[callback] error:', err);
router.replace('/login?error=auth_failed');
});
}, [router]);
return (
<div style={{ display:'flex', alignItems:'center', justifyContent:'center', height:'100vh' }}>
<p>Autenticando...</p>
</div>
);
}
Passo 3 — Proteger rotas
Layout que verifica se o usuário está autenticado antes de renderizar:
// src/app/(protected)/layout.tsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { getUser, login, type User } from '@/lib/auth';
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const [user, setUser] = useState<User | null | undefined>(undefined);
useEffect(() => {
getUser().then((u) => {
if (!u || u.expired) {
login(); // redireciona para o Auth Platform
} else {
setUser(u);
}
});
}, []);
// Aguardando verificação
if (user === undefined) {
return <div style={{ display:'flex', alignItems:'center', justifyContent:'center', height:'100vh' }}>
Carregando...
</div>;
}
return <>{children}</>;
}
Passo 4 — Exibir dados do usuário
// src/app/(protected)/dashboard/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { getUser, logout, type User } from '@/lib/auth';
export default function DashboardPage() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
getUser().then(setUser);
}, []);
if (!user) return null;
// Claims do JWT — disponíveis sem chamada extra à API
const claims = user.profile as any;
return (
<div>
<h1>Olá, {claims.email}</h1>
<dl>
<dt>userId (sub)</dt> <dd>{claims.sub}</dd>
<dt>conta ativa</dt> <dd>{claims.account_id}</dd>
<dt>é admin?</dt> <dd>{claims.is_admin ? 'Sim' : 'Não'}</dd>
<dt>roles</dt> <dd>{claims.roles?.join(', ')}</dd>
<dt>escopos de recurso</dt> <dd>{claims.resource_scopes?.join(', ')}</dd>
</dl>
<button onClick={() => logout()}>Sair</button>
</div>
);
}
Passo 5 — Chamar a sua API
// src/lib/api.ts
import { getToken, login } from './auth';
export async function apiFetch<T>(
url: string,
options?: RequestInit,
): Promise<T> {
const token = await getToken();
// Usuário não autenticado — redireciona para login
if (!token) {
await login();
throw new Error('não autenticado');
}
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...options?.headers,
},
});
// Token expirado (não renovado silenciosamente)
if (res.status === 401) {
await login();
throw new Error('sessão expirada');
}
if (!res.ok) throw new Error(`Erro ${res.status}`);
return res.json() as Promise<T>;
}
// Uso:
// const orders = await apiFetch<Order[]>('/api/orders');
Passo 6 — Logout completo
// Chama signoutRedirect — o Auth Platform:
// 1. Revoga o refresh_token no Redis
// 2. Encerra a sessão do servidor
// 3. Redireciona para post_logout_redirect_uri
await logout();
// → usuário é redirecionado para "/"
Variáveis de ambiente
# .env.local
NEXT_PUBLIC_AUTH_URL=http://localhost:4000
NEXT_PUBLIC_CLIENT_ID=dc867d92-945d-43fb-b2de-3aa5608aee03
Checklist
-
CLIENT_IDregistrado no Admin Console com oredirect_uricorreto - Página
/callbackcriada e processandosigninRedirectCallback() - Rotas protegidas verificando
user.expired -
automaticSilentRenew: truepara não interromper a sessão do usuário - Logout usando
signoutRedirect()(não apenas limpar sessionStorage)
Exemplo funcional:
examples/01-nextjs-pkce(Next.js) eexamples/react-spa(React + Vite)