Pular para o conteúdo principal

Adicionar autenticação ao seu aplicativo Next.js (App Router)

dica:

Pré-requisitos

Instalação

Importe o Logto SDK através do seu gerenciador de pacotes favorito:

npm i @logto/next

Integração

Preparar configurações

Prepare a configuração para o cliente Logto:

app/logto.ts
import { LogtoNextConfig } from '@logto/next';

export const logtoConfig: LogtoNextConfig = {
appId: '<your-application-id>',
appSecret: '<your-app-secret-copied-from-console>',
endpoint: '<your-logto-endpoint>', // Ex.: http://localhost:3001
baseUrl: '<your-nextjs-app-base-url>', // Ex.: http://localhost:3000
cookieSecret: 'complex_password_at_least_32_characters_long',
cookieSecure: process.env.NODE_ENV === 'production',
};

Nota:
Se você estiver usando uma variável de ambiente para cookieSecret (por exemplo, process.env.LOGTO_COOKIE_SECRET), certifique-se de que o valor tenha pelo menos 32 caracteres. Se esse requisito não for atendido, o Logto lançará o seguinte erro durante a build ou execução:

TypeError: Either sessionWrapper or encryptionKey must be provided for CookieStorage

Para evitar esse erro, certifique-se de que a variável de ambiente esteja corretamente definida ou forneça um valor padrão com comprimento mínimo de 32 caracteres.

Configurar URIs de redirecionamento

Antes de entrarmos nos detalhes, aqui está uma visão geral rápida da experiência do usuário final. O processo de login pode ser simplificado da seguinte forma:

  1. Seu app invoca o método de login.
  2. O usuário é redirecionado para a página de login do Logto. Para aplicativos nativos, o navegador do sistema é aberto.
  3. O usuário faz login e é redirecionado de volta para seu app (configurado como o URI de redirecionamento).

Sobre o login baseado em redirecionamento

  1. Este processo de autenticação segue o protocolo OpenID Connect (OIDC), e o Logto aplica medidas de segurança rigorosas para proteger o login do usuário.
  2. Se você tiver vários aplicativos, pode usar o mesmo provedor de identidade (Logto). Uma vez que o usuário faz login em um aplicativo, o Logto completará automaticamente o processo de login quando o usuário acessar outro aplicativo.

Para saber mais sobre a lógica e os benefícios do login baseado em redirecionamento, veja Experiência de login do Logto explicada.


nota:

Nos trechos de código a seguir, assumimos que seu aplicativo está sendo executado em http://localhost:3000/.

Configurar URIs de redirecionamento

Vá para a página de detalhes do aplicativo no Logto Console. Adicione um URI de redirecionamento http://localhost:3000/callback.

URI de redirecionamento no Logto Console

Assim como no login, os usuários devem ser redirecionados para o Logto para sair da sessão compartilhada. Uma vez concluído, seria ótimo redirecionar o usuário de volta para o seu site. Por exemplo, adicione http://localhost:3000/ como a seção de URI de redirecionamento pós logout.

Em seguida, clique em "Salvar" para salvar as alterações.

Tratar o callback

Após o usuário fazer login, o Logto irá redirecioná-lo de volta para o URI de redirecionamento configurado acima. No entanto, ainda há coisas a serem feitas para que seu aplicativo funcione corretamente.

Nós fornecemos uma função auxiliar handleSignIn para tratar o callback de login:

app/callback/route.ts
import { handleSignIn } from '@logto/next/server-actions';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
import { logtoConfig } from '../logto';

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
await handleSignIn(logtoConfig, searchParams);

redirect('/');
}

Implementar login e logout

Implementar botão de login e logout

No Next.js App Router, eventos são tratados em componentes de cliente, então precisamos criar dois componentes primeiro: SignIn e SignOut.

app/sign-in.tsx
'use client';

type Props = {
onSignIn: () => Promise<void>;
};

const SignIn = ({ onSignIn }: Props) => {
return (
<button
onClick={() => {
onSignIn();
}}
>
Sign In
</button>
);
};

export default SignIn;
app/sign-out.tsx
'use client';

type Props = {
onSignOut: () => Promise<void>;
};

const SignOut = ({ onSignOut }: Props) => {
return (
<button
onClick={() => {
onSignOut();
}}
>
Sign Out
</button>
);
};

export default SignOut;

Lembre-se de adicionar 'use client' no topo do arquivo para indicar que esses componentes são componentes de cliente.

Adicionar botões à página inicial

nota:

Não é permitido definir Ações do Servidor anotadas com "use server" em Componentes de Cliente. Temos que passá-las através de props de um Componente de Servidor.

Agora vamos adicionar os botões de login e logout na sua página inicial. Precisamos chamar as server actions do SDK quando necessário. Para ajudar nisso, use getLogtoContext para buscar o status de autenticação.

app/page.tsx
import { getLogtoContext, signIn, signOut } from '@logto/next/server-actions';
import SignIn from './sign-in';
import SignOut from './sign-out';
import { logtoConfig } from './logto';

export default async function Home() {
const { isAuthenticated, claims } = await getLogtoContext(logtoConfig);

return (
<nav>
{isAuthenticated ? (
<p>
Olá, {claims?.sub},
<SignOut
onSignOut={async () => {
'use server';

await signOut(logtoConfig);
}}
/>
</p>
) : (
<p>
<SignIn
onSignIn={async () => {
'use server';

await signIn(logtoConfig);
}}
/>
</p>
)}
</nav>
);
}

Ponto de verificação: Teste seu aplicativo

Agora, você pode testar seu aplicativo:

  1. Execute seu aplicativo, você verá o botão de login.
  2. Clique no botão de login, o SDK iniciará o processo de login e redirecionará você para a página de login do Logto.
  3. Após fazer login, você será redirecionado de volta para seu aplicativo e verá o botão de logout.
  4. Clique no botão de logout para limpar o armazenamento de tokens e sair.

Buscar informações do usuário

Exibir informações do usuário

Quando o usuário está autenticado, o valor de retorno de getLogtoContext() será um objeto contendo as informações do usuário. Você pode exibir essas informações em seu aplicativo:

app/page.tsx
import { getLogtoContext } from '@logto/next/server-actions';
import { logtoConfig } from './logto';

export default async function Home() {
const { claims } = await getLogtoContext(logtoConfig);

return (
<main>
{claims && (
<div>
<h2>Reivindicações (Claims):</h2>
<table>
<thead>
<tr>
<th>Nome</th>
<th>Valor</th>
</tr>
</thead>
<tbody>
{Object.entries(claims).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{String(value)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}

Obter informações do usuário em manipuladores de rotas de API

Você também pode obter informações do usuário em manipuladores de rotas de API:

app/api/profile/route.ts
import { getLogtoContext } from '@logto/next/server-actions';
import { logtoConfig } from '../../logto';

export const dynamic = 'force-dynamic';

export async function GET() {
const { claims } = await getLogtoContext(logtoConfig);

return Response.json({ claims });
}

Solicitar reivindicações adicionais

Você pode perceber que algumas informações do usuário estão faltando no objeto retornado de getLogtoContext. Isso ocorre porque OAuth 2.0 e OpenID Connect (OIDC) são projetados para seguir o princípio do menor privilégio (PoLP), e o Logto é construído com base nesses padrões.

Por padrão, reivindicações limitadas são retornadas. Se você precisar de mais informações, pode solicitar escopos adicionais para acessar mais reivindicações.

info:

Uma "reivindicação (Claim)" é uma afirmação feita sobre um sujeito; um "escopo (Scope)" é um grupo de reivindicações. No caso atual, uma reivindicação é uma informação sobre o usuário.

Aqui está um exemplo não normativo da relação escopo - reivindicação:

dica:

A reivindicação "sub" significa "sujeito (Subject)", que é o identificador único do usuário (ou seja, ID do usuário).

O Logto SDK sempre solicitará três escopos: openid, profile e offline_access.

Para solicitar escopos adicionais, você pode configurar os parâmetros ao iniciar o cliente Logto:

app/logto.ts
import { UserScope, LogtoNextConfig } from '@logto/next';

export const logtoConfig: LogtoNextConfig = {
scopes: [UserScope.Email, UserScope.Phone], // Adicione mais escopos se necessário
// ...outras configurações
});

Então você pode acessar as reivindicações adicionais na resposta do contexto:

app/page.tsx
export default async function Home() {
const { claims: { email } = {}, } = await getLogtoContext(logtoConfig);

return (
<div>
{email && <p>Email: {email}</p>}
</div>
);
};

export default Home;

Reivindicações que precisam de solicitações de rede

Para evitar o inchaço do Token de ID (ID token), algumas reivindicações requerem solicitações de rede para serem buscadas. Por exemplo, a reivindicação custom_data não está incluída no objeto do usuário, mesmo que seja solicitada nos escopos. Para acessar essas reivindicações, você pode configurar a opção fetchUserInfo:

app/page.tsx
export default async function Home() {
const { userInfo } = await getLogtoContext(logtoConfig, { fetchUserInfo: true });
return (
<div>
{userInfo && <p>Email: {userInfo.email}</p>}
</div>
);
};

export default Home;
Ao configurar fetchUserInfo, o SDK buscará as informações do usuário solicitando ao endpoint userinfo após o usuário fazer login, e userInfo estará disponível assim que a solicitação for concluída.

Escopos e reivindicações

Logto utiliza as convenções de escopos e reivindicações (scopes and claims) do OIDC para definir os escopos e reivindicações para obtenção de informações do usuário a partir do token de ID (ID token) e do endpoint userinfo do OIDC. Tanto "escopo (Scope)" quanto "reivindicação (Claim)" são termos das especificações do OAuth 2.0 e OpenID Connect (OIDC).

Para reivindicações padrão do OIDC, a inclusão no token de ID é estritamente determinada pelos escopos solicitados. Reivindicações estendidas (como custom_data e organizations) podem ser configuradas adicionalmente para aparecer no token de ID através das configurações de Token de ID personalizado.

Aqui está a lista de escopos suportados e as reivindicações correspondentes:

Escopos OIDC padrão

openid (padrão)

Nome da reivindicaçãoTipoDescrição
substringO identificador único do usuário

profile (padrão)

Nome da reivindicaçãoTipoDescrição
namestringO nome completo do usuário
usernamestringO nome de usuário do usuário
picturestringURL da foto de perfil do usuário final. Esta URL DEVE se referir a um arquivo de imagem (por exemplo, um arquivo de imagem PNG, JPEG ou GIF), em vez de uma página da Web contendo uma imagem. Observe que esta URL DEVE referenciar especificamente uma foto de perfil do usuário final adequada para exibição, em vez de uma foto arbitrária tirada pelo usuário final.
created_atnumberMomento em que o usuário final foi criado. O tempo é representado como o número de milissegundos desde a época Unix (1970-01-01T00:00:00Z).
updated_atnumberMomento em que as informações do usuário final foram atualizadas pela última vez. O tempo é representado como o número de milissegundos desde a época Unix (1970-01-01T00:00:00Z).

Outras reivindicações padrão incluem family_name, given_name, middle_name, nickname, preferred_username, profile, website, gender, birthdate, zoneinfo e locale também serão incluídas no escopo profile sem a necessidade de solicitar o endpoint userinfo. Uma diferença em relação às reivindicações acima é que essas reivindicações só serão retornadas quando seus valores não forem vazios, enquanto as reivindicações acima retornarão null se os valores estiverem vazios.

nota:

Diferente das reivindicações padrão, as reivindicações created_at e updated_at usam milissegundos em vez de segundos.

email

Nome da reivindicaçãoTipoDescrição
emailstringO endereço de email do usuário
email_verifiedbooleanSe o endereço de email foi verificado

phone

Nome da reivindicaçãoTipoDescrição
phone_numberstringO número de telefone do usuário
phone_number_verifiedbooleanSe o número de telefone foi verificado

address

Consulte o OpenID Connect Core 1.0 para detalhes sobre a reivindicação de endereço.

info:

Escopos marcados como (padrão) são sempre solicitados pelo SDK do Logto. As reivindicações sob escopos OIDC padrão são sempre incluídas no token de ID quando o escopo correspondente é solicitado — elas não podem ser desativadas.

Escopos estendidos

Os seguintes escopos são estendidos pelo Logto e retornarão reivindicações através do endpoint userinfo. Essas reivindicações também podem ser configuradas para serem incluídas diretamente no token de ID através de Console > Custom JWT. Veja Token de ID personalizado para mais detalhes.

custom_data

Nome da reivindicaçãoTipoDescriçãoIncluído no token de ID por padrão
custom_dataobjectOs dados personalizados do usuário

identities

Nome da reivindicaçãoTipoDescriçãoIncluído no token de ID por padrão
identitiesobjectAs identidades vinculadas do usuário
sso_identitiesarrayAs identidades SSO vinculadas do usuário

roles

Nome da reivindicaçãoTipoDescriçãoIncluído no token de ID por padrão
rolesstring[]Os papéis do usuário

urn:logto:scope:organizations

Nome da reivindicaçãoTipoDescriçãoIncluído no token de ID por padrão
organizationsstring[]Os IDs das organizações às quais o usuário pertence
organization_dataobject[]Os dados das organizações às quais o usuário pertence
nota:

Essas reivindicações de organização também podem ser recuperadas via endpoint userinfo ao usar um token opaco. No entanto, tokens opacos não podem ser usados como tokens de organização para acessar recursos específicos da organização. Veja Token opaco e organizações para mais detalhes.

urn:logto:scope:organization_roles

Nome da reivindicaçãoTipoDescriçãoIncluído no token de ID por padrão
organization_rolesstring[]Os papéis da organização aos quais o usuário pertence no formato <organization_id>:<role_name>

Recursos de API

Recomendamos ler 🔐 Controle de Acesso Baseado em Papel (RBAC) primeiro para entender os conceitos básicos do RBAC do Logto e como configurar corretamente os recursos de API.

Configurar o cliente Logto

Depois de configurar os recursos de API, você pode adicioná-los ao configurar o Logto em seu aplicativo:

app/logto.ts
export const logtoConfig = {
// ...other configs
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // Adicionar recursos de API
};

Cada recurso de API tem suas próprias permissões (escopos).

Por exemplo, o recurso https://shopping.your-app.com/api tem as permissões shopping:read e shopping:write, e o recurso https://store.your-app.com/api tem as permissões store:read e store:write.

Para solicitar essas permissões, você pode adicioná-las ao configurar o Logto em seu aplicativo:

app/logto.ts
export const logtoConfig = {
// ...other configs
scopes: ['shopping:read', 'shopping:write', 'store:read', 'store:write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'],
};

Você pode notar que os escopos são definidos separadamente dos recursos de API. Isso ocorre porque Resource Indicators for OAuth 2.0 especifica que os escopos finais para a solicitação serão o produto cartesiano de todos os escopos em todos os serviços de destino.

Assim, no caso acima, os escopos podem ser simplificados a partir da definição no Logto, ambos os recursos de API podem ter escopos read e write sem o prefixo. Então, na configuração do Logto:

app/logto.ts
export const logtoConfig = {
// ...other configs
scopes: ['read', 'write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'],
};

Para cada recurso de API, ele solicitará os escopos read e write.

nota:

Não há problema em solicitar escopos que não estão definidos nos recursos de API. Por exemplo, você pode solicitar o escopo email mesmo que os recursos de API não tenham o escopo email disponível. Escopos indisponíveis serão ignorados com segurança.

Após o login bem-sucedido, o Logto emitirá os escopos apropriados para os recursos de API de acordo com os papéis do usuário.

Buscar token de acesso para o recurso de API

Para buscar o token de acesso para um recurso de API específico, você pode usar o método getAccessToken:

nota:

Não é permitido definir Ações do Servidor anotadas com "use server" em Componentes de Cliente. Temos que passá-las através de props de um Componente de Servidor.

app/page.ts
import { getAccessToken } from '@logto/next/server-actions';
import { logtoConfig } from './logto';
import GetAccessToken from './get-access-token';

export default async function Home() {
return (
<main>
<GetAccessToken
onGetAccessToken={async () => {
'use server';

return getAccessToken(logtoConfig, 'https://shopping.your-app.com/api');
}}
/>
</main>
);
}
app/get-access-token.ts
'use client';

type Props = {
onGetAccessToken: () => Promise<string>;
};

const GetAccessToken = ({ onGetAccessToken }: Props) => {
return (
<button
onClick={async () => {
const token = await onGetAccessToken();
console.log(token);
}}
>
Obter token de acesso (veja o log do console)
</button>
);
};

export default GetAccessToken;

Este método retornará um token de acesso JWT que pode ser usado para acessar o recurso de API quando o usuário tiver as permissões relacionadas. Se o token de acesso em cache atual tiver expirado, este método tentará automaticamente usar um token de atualização para obter um novo token de acesso.

Se você precisar buscar um token de acesso em um componente de servidor, pode usar a função getAccessTokenRSC:

app/page.tsx
// Importações permanecem inalteradas
import { getAccessTokenRSC } from '@logto/next/server-actions';
import { logtoConfig } from './logto';

export default async function Home() {
const accessToken = await getAccessTokenRSC(logtoConfig, 'https://shopping.your-app.com/api');

return (
<main>
<p>Token de acesso: {accessToken}</p>
</main>
);
}
Limitação de cache de token em RSC:

React Server Components não podem gravar cookies (limitação do Next.js). Embora getAccessTokenRSC ainda atualize tokens expirados usando o token de atualização, o novo token de acesso não será persistido no cookie de sessão. Isso significa que cada requisição RSC pode acionar uma atualização de token se o token em cache estiver expirado.

Soluções:

  1. Use componentes de cliente com Server Actions - Chame getAccessToken de um componente de cliente via Server Actions, que pode atualizar cookies.
  2. Use armazenamento de sessão externo - Configure um sessionWrapper com Redis/armazenamento KV. O cookie armazena apenas um ID de sessão fixo enquanto os dados do token ficam no armazenamento externo, permitindo que o RSC persista tokens atualizados. Veja Usar armazenamento de sessão externo abaixo.

Buscar tokens de organização

Se organização é um conceito novo para você, por favor, leia 🏢 Organizações (Multi-tenancy) para começar.

Você precisa adicionar o escopo UserScope.Organizations ao configurar o cliente Logto:

app/logto.ts
import { UserScope } from '@logto/next';

export const logtoConfig = {
// ...other configs
scopes: [UserScope.Organizations],
};

Uma vez que o usuário esteja autenticado, você pode buscar o token de organização para o usuário:

nota:

Não é permitido definir Ações do Servidor anotadas com "use server" em Componentes de Cliente. Temos que passá-las através de props de um Componente de Servidor.

app/page.ts
import { getOrganizationToken } from '@logto/next/server-actions';
import { logtoConfig } from './logto';
import GetOrganizationToken from './get-organization-token';

export default async function Home() {
return (
<main>
<GetOrganizationToken
onGetOrganizationToken={async () => {
'use server';

return getOrganizationToken(logtoConfig, 'organization-id');
}}
/>
</main>
);
}
app/get-organization-token.ts
'use client';

type Props = {
onGetOrganizationToken: () => Promise<string>;
};

const GetOrganizationToken = ({ onGetOrganizationToken }: Props) => {
return (
<button
onClick={async () => {
const token = await onGetOrganizationToken();
console.log(token);
}}
>
Obter token de organização (veja o log do console)
</button>
);
};

export default GetOrganizationToken;

Se você precisar buscar um token de organização em um componente de servidor, pode usar a função getOrganizationTokenRSC:

app/page.tsx
// Importações permanecem inalteradas
import { getOrganizationTokenRSC } from '@logto/next/server-actions';
import { logtoConfig } from './logto';

export default async function Home() {
const token = await getOrganizationTokenRSC(logtoConfig, 'organization-id');

return (
<main>
<p>Token de organização: {token}</p>
</main>
);
}
Limitação de cache de token em RSC:

Assim como o getAccessTokenRSC, o token de organização atualizado não será persistido em RSC. Veja soluções acima.

Usar armazenamento de sessão externo

O SDK utiliza cookies para armazenar dados de sessão criptografados por padrão. Essa abordagem é segura, não requer infraestrutura adicional e funciona especialmente bem em ambientes serverless como o Vercel.

No entanto, há momentos em que pode ser necessário armazenar os dados de sessão externamente. Por exemplo, quando seus dados de sessão ficam grandes demais para os cookies, especialmente quando você precisa manter várias sessões de organização ativas simultaneamente. Nesses casos, você pode implementar um armazenamento externo de sessão usando a opção sessionWrapper:

import { MemorySessionWrapper } from './storage';

export const config = {
// ...
sessionWrapper: new MemorySessionWrapper(),
};
import { randomUUID } from 'node:crypto';

import { type SessionWrapper, type SessionData } from '@logto/next';

export class MemorySessionWrapper implements SessionWrapper {
private readonly storage = new Map<string, unknown>();
private currentSessionId?: string;

async wrap(data: unknown, _key: string): Promise<string> {
// Reutiliza o ID de sessão existente se disponível, gerando um novo apenas para usuários de primeira viagem.
// Isso é importante para ambientes onde os cookies não podem ser atualizados (por exemplo, React Server Components),
// pois o ID da sessão no cookie deve permanecer estável enquanto os dados no armazenamento externo podem ser atualizados.
const sessionId = this.currentSessionId ?? randomUUID();
this.currentSessionId = sessionId;
this.storage.set(sessionId, data);
return sessionId;
}

async unwrap(value: string, _key: string): Promise<SessionData> {
if (!value) {
return {};
}

// Armazena o ID da sessão para possível reutilização em wrap()
this.currentSessionId = value;
const data = this.storage.get(value);
return data ?? {};
}
}

A implementação acima utiliza um armazenamento simples em memória. Em um ambiente de produção, você pode querer usar uma solução de armazenamento mais persistente, como Redis ou um banco de dados.

Redirecionamento automático para login ao não autorizado

dica:

O helper signIn modifica cookies para estabelecer a sessão de login, portanto não pode ser invocado diretamente em um React Server Component (RSC). Para acionar o login automaticamente quando um RSC detectar um usuário não autorizado, chame signIn em um handler de rota dedicado e redirecione para essa rota.

app/sign-in/route.ts
// Importações permanecem inalteradas
import { signIn } from '@logto/next/server-actions';
import { logtoConfig } from '../../logto';

export async function GET() {
await signIn(logtoConfig);
}
app/protected/page.tsx
// Importações permanecem inalteradas
import { getLogtoContext } from '@logto/next/server-actions';
import { redirect } from 'next/navigation';
import { logtoConfig } from '../logto';

export default async function ProtectedPage() {
const { isAuthenticated } = await getLogtoContext(logtoConfig);

if (!isAuthenticated) {
redirect('/sign-in');
}

return <div>Conteúdo protegido</div>;
}

Leituras adicionais

Fluxos do usuário final: fluxos de autenticação, fluxos de conta e fluxos de organização Configurar conectores Autorização (Authorization)