Next.js
Use o authio-provider-nextauth em produção: Auth.js v5 ou NextAuth v4, escopos, claims tipadas, refresh tokens, rotas protegidas e login em popup.
O authio-provider-nextauth é o provider do Authio para o Auth.js v5 e o NextAuth v4, versão 2.1.0 no npm. Ele é construído sobre o provider OpenID Connect padrão de cada versão, então discovery, PKCE e validação do ID token vêm do próprio Auth.js; o pacote acrescenta os padrões do Authio, as claims tipadas e os helpers de popup.
Esta página continua de onde o Início rápido para: o ponto de entrada certo, mais escopos, sessões mais longas e rotas que só quem entrou alcança.
Escolha o ponto de entrada
| Sua versão do next-auth | Importe de | Construído sobre |
|---|---|---|
5.0.0-beta (Auth.js) | authio-provider-nextauth/v5 | O tipo de provider oidc; lê as configurações do ambiente. |
^4.24.0 | authio-provider-nextauth | O tipo de provider oauth com discovery; toda configuração é passada no código. |
| Qualquer uma | authio-provider-nextauth/popup | Os helpers de login em popup, que não importam nada do next-auth. |
As duas versões exportam o mesmo provider Authio e o mesmo tipo AuthioProfile. O next-auth é uma peer dependency: instale a versão que você usa e fixe a beta do Auth.js em vez de seguir a tag beta, que pula entre versões incompatíveis.
npm install authio-provider-nextauth next-auth@5.0.0-beta.32Variáveis de ambiente
| Variável | O que guarda |
|---|---|
AUTH_AUTHIO_ISSUER | O emissor do realm, como https://authio.api.hyperio.tech/realms/acme. |
AUTH_AUTHIO_ID | O ID do client, como acme-web. |
AUTH_AUTHIO_SECRET | O segredo do client. Deixe de fora num client público. |
AUTH_SECRET | A chave que cifra o cookie de sessão, gerada com npx auth secret. NEXTAUTH_SECRET no v4. |
AUTH_URL | O endereço público da aplicação, como https://app.example.com. NEXTAUTH_URL no v4. |
O Auth.js v5 lê sozinho as três variáveis AUTH_AUTHIO_: ele coloca o ID do provider, authio, em maiúsculas e procura exatamente esses nomes. No v4, passe os valores para Authio() no código.
Duas variáveis que costumam ser confundidas
AUTH_URL, não NEXTAUTH_URL: só AUTH_URL (ou AUTH_TRUST_HOST=true) marca o host como confiável, e sem isso um deploy próprio falha com UntrustedHost. E AUTH_SECRET nunca é o segredo do client: com um valor só para os dois, girar o segredo do client derruba todas as sessões abertas.Configure o Auth.js v5
Com as variáveis no lugar, o provider não precisa de credenciais no código. Acrescente a verificação de state: o ponto de entrada v5 não define checks, então o Auth.js fica só com o PKCE, e é o state que amarra o retorno ao pedido que o iniciou. As opções que você passar são mescladas sobre o ambiente.
import NextAuth from 'next-auth'
import Authio from 'authio-provider-nextauth/v5'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [Authio({ checks: ['pkce', 'state'] })],
})O redirect URI a registrar no client é https://app.example.com/api/auth/callback/authio, um por ambiente.
Configure o NextAuth v4
No v4, issuer, clientId e clientSecret são passados explicitamente; issuer é obrigatório, porque é dele que sai o endereço de discovery. O ponto de entrada já pede openid email profile, liga as verificações pkce e state e mapeia sub, name (ou preferred_username), email e picture para o usuário.
import type { NextAuthOptions } from 'next-auth'
import { Authio } from 'authio-provider-nextauth'
export const authOptions: NextAuthOptions = {
providers: [
Authio({
issuer: process.env.AUTH_AUTHIO_ISSUER!,
clientId: process.env.AUTH_AUTHIO_ID!,
clientSecret: process.env.AUTH_AUTHIO_SECRET!,
}),
],
}Escopos
Os dois pontos de entrada pedem openid profile email. Todo escopo que você pedir também precisa estar liberado no client em Clients.
| Escopo | O que acrescenta |
|---|---|
openid | O ID token. Obrigatório, a menos que o client desligue Require OpenID Connect. |
profile, email, phone | Nomes e username; o endereço e se ele foi verificado; o telefone. |
roles | Os papéis do usuário no access token, para a sua API conferir — veja Papéis e permissões. |
offline_access | Um refresh token — veja Refresh tokens. |
Sobrescreva authorization.params.scope para mudar a lista. As opções são mescladas chave por chave, mas scope é uma string única e é trocada por inteiro, então escreva a lista completa:
Authio({
checks: ['pkce', 'state'],
authorization: {
params: { scope: 'openid profile email roles offline_access' },
},
})Para chamar uma API sua registrada como servidor de recurso, acrescente o identificador dela, como https://api.example.com, como resource nos mesmos params. O access token passa a levá-lo em aud no lugar do padrão Authio, e as renovações o mantêm.
Claims tipadas
AuthioProfile tipa as claims do ID token que o Auth.js validou. O argumento profile do callback jwt só as traz no login, então copie para o token o que você precisar ali.
| Claims | Quando | O que dizem |
|---|---|---|
sub | Sempre | O ID estável do usuário no realm. |
name, given_name, family_name, preferred_username, email, email_verified | Escopos profile e email | Quem a pessoa é. |
sid, auth_time, acr, amr | Sempre | A sessão, quando a pessoa se autenticou, com que força (1 a 3) e com quais métodos (pwd, otp, swk, fed, mfa…). |
idp, org_id, org_alias | Login federado ou para uma organização | O alias do provedor externo; a organização da sessão. |
As claims acrescentadas pelos claim mappers do seu client também chegam, tipadas como unknown pela assinatura de índice. Declare uma vez o que você guarda no token e na sessão:
import type { DefaultSession } from 'next-auth'
import 'next-auth/jwt'
declare module 'next-auth' {
interface Session {
error?: string
user: { username?: string } & DefaultSession['user']
}
}
declare module 'next-auth/jwt' {
interface JWT {
username?: string
accessToken?: string
expiresAt?: number
refreshToken?: string
error?: string
}
}session devolve é servido ao navegador por /api/auth/session. Coloque o access token ali só se código no navegador precisar chamar a sua API com ele.Clients confidenciais e públicos
Uma aplicação Next.js troca o código no próprio servidor, então o comum é um client confidencial. Se o client for público (método none), declare isso: sem a declaração, o Auth.js ainda manda um cabeçalho Basic com a senha literal undefined, que o Authio ignora num client público — até o client virar confidencial. No v4, passe também clientSecret: '' para satisfazer os tipos.
Authio({
checks: ['pkce', 'state'],
client: { token_endpoint_auth_method: 'none' },
})Refresh tokens
O Authio só emite refresh_token quando os escopos concedidos incluem offline_access — nenhuma configuração de tempo de vida gera um. Libere o escopo no client, peça-o e renove o access token você mesmo no callback jwt:
import NextAuth from 'next-auth'
import Authio from 'authio-provider-nextauth/v5'
async function refresh(refreshToken: string) {
const credentials = btoa(`${process.env.AUTH_AUTHIO_ID}:${process.env.AUTH_AUTHIO_SECRET}`)
const response = await fetch(`${process.env.AUTH_AUTHIO_ISSUER}/protocol/openid-connect/token`, {
method: 'POST',
headers: {
authorization: `Basic ${credentials}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
})
if (!response.ok) return null
return (await response.json()) as {
access_token: string
expires_in: number
refresh_token?: string
}
}
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Authio({
checks: ['pkce', 'state'],
authorization: { params: { scope: 'openid profile email offline_access' } },
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
return {
...token,
accessToken: account.access_token,
expiresAt: account.expires_at,
refreshToken: account.refresh_token,
}
}
if (token.expiresAt && Date.now() < (token.expiresAt - 60) * 1000) return token
const refreshed = token.refreshToken ? await refresh(token.refreshToken) : null
if (!refreshed) return { ...token, error: 'RefreshTokenError' }
return {
...token,
accessToken: refreshed.access_token,
expiresAt: Math.floor(Date.now() / 1000) + refreshed.expires_in,
refreshToken: refreshed.refresh_token ?? token.refreshToken,
error: undefined,
}
},
async session({ session, token }) {
session.error = token.error
return session
},
},
})- Guarde o novo refresh token. Refresh tokens são rotacionados por padrão; o anterior fica gasto. Dentro da janela de reuso (10 segundos por padrão), uma renovação repetida recebe o mesmo substituto; um token gasto apresentado depois disso encerra a sessão, como sinal de roubo.
- Sem novo ID token. A renovação só devolve
id_tokense o client ligar ID Token on Refresh. - Os tempos de vida são por client. O refresh token vale 60 minutos por padrão.
- Clients públicos mandam
client_idno corpo em vez do cabeçalhoAuthorization.
Quando session.error valer RefreshTokenError, comece o login de novo: se a sessão da pessoa no Authio ainda estiver viva, ela passa direto.
Proteja rotas
Envolva o auth no proxy para mandar quem não tem sessão para o login antes de a página renderizar. No Next.js 16 o arquivo é proxy.ts; no Next.js 15 e anteriores, é middleware.ts e o export se chama middleware.
import { NextResponse } from 'next/server'
import { auth } from '@/auth'
export const proxy = auth((req) => {
if (!req.auth) {
const signInUrl = new URL('/api/auth/signin', req.nextUrl.origin)
signInUrl.searchParams.set('callbackUrl', req.nextUrl.href)
return NextResponse.redirect(signInUrl)
}
})
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*'],
}O proxy decide o que renderiza, não quais dados podem ser lidos: chame auth() de novo nos server components, server actions e route handlers que devolvem dados protegidos, e deixe a sua API validar o access token por conta própria — veja .NET ou validação de tokens.
Login em popup
O authio-provider-nextauth/popup mantém a página onde está e faz o login num popup aberto sobre ela. Dentro dele roda o fluxo normal: o Auth.js troca o código no seu servidor, o cookie de sessão cai na sua origem, e o Authio recebe display=popup e ajusta as telas à janela. Bastam duas páginas pequenas, sem redirect URI extra.
'use client'
import { useEffect } from 'react'
import { signIn } from 'next-auth/react'
import { startPopupSignIn } from 'authio-provider-nextauth/popup'
export default function PopupStart() {
useEffect(() => {
startPopupSignIn(signIn)
}, [])
return null
}signInWithPopupnunca rejeita: resolve{ ok: true }ou{ ok: false, error }compopup_blocked(chame direto do clique),popup_closed,timeout(5 minutos) ou o erro do Auth.js.- As páginas são
/auth/popupe/auth/popup/donepor padrão, na origem do botão; mude comsignInWithPopup({ startPath })estartPopupSignIn(signIn, { donePath }). - Um login recusado cai na página de erro do Auth.js: aponte
pages.errorpara uma página que chamecompletePopupSignIn()e o botão recebe o erro. - Não sirva a página com
Cross-Origin-Opener-Policy: same-origin; usesame-origin-allow-popups.
O pacote não usa response_mode=web_message: o popup volta por /api/auth/callback/authio como qualquer login, e a sua própria página de conclusão avisa o botão. Sem o Auth.js, veja o fluxo em popup no protocolo.
Solução de problemas
| O que aparece | Por quê | O que fazer |
|---|---|---|
Nenhum refresh_token na resposta | offline_access não foi concedido. | Libere o escopo no client e acrescente-o à string scope completa. |
UntrustedHost em produção | NEXTAUTH_URL definido no lugar de AUTH_URL no v5. | Defina AUTH_URL com o endereço público da aplicação. |
Please define a secret | AUTH_SECRET está vazio. | Gere um com npx auth secret. |
| Erros de tipo ao importar a entrada raiz numa aplicação v5 | A entrada v4 importa next-auth/providers/oauth, que não existe no v5. | Importe de authio-provider-nextauth/v5. |