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 clientId do 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_ID registrado no Admin Console com o redirect_uri correto
  • Página /callback criada e processando signinRedirectCallback()
  • Rotas protegidas verificando user.expired
  • automaticSilentRenew: true para 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) e examples/react-spa (React + Vite)