5 min de leituraPortuguês

Autenticação Segura com Microsoft Entra ID para desenvolvedores Typescript

Aplicações modernas funcionam raramente isoladas. Muitas vezes temos um frontend que lida com a...

  • #webdev
  • #programming
  • #nextjs
  • #azure

Publicado originalmente no dev.to (abre em nova aba) por Cláudio Rapôso. Comentários e reações ficam por lá.

Aplicações modernas funcionam raramente isoladas. Muitas vezes temos um frontend que lida com a experiência do usuário, e um backend que concentra as regras de negócio e o acesso a dados sensíveis.
Garantir que apenas usuários autenticados consigam acessar esses recursos é fundamental.

Neste artigo, vamos construir uma solução que utiliza Next.js como frontend, integrando a autenticação com o Microsoft Entra ID (antigo Azure Active Directory), e um backend em Node/TypeScript que valida tokens JWT emitidos pelo Entra ID antes de liberar o acesso a rotas protegidas.


Arquitetura Geral

A solução pode ser entendida em cinco passos principais:

Diagrama de Sequência

  1. O usuário acessa a aplicação em Next.js e inicia o login.
  2. O NextAuth.js redireciona o usuário para o Microsoft Entra ID.
  3. Após a autenticação, o Entra ID retorna um ID Token e um Access Token para o Next.js.
  4. O frontend envia o Access Token ao backend, no header Authorization: Bearer <token>.
  5. O backend valida o token contra o serviço de chaves públicas (JWKS) do Entra ID.
  • Se válido → rota protegida é acessada.
  • Se inválido/expirado → resposta 401 Unauthorized.

1. Configuração no Entra ID

No Azure Portal:

  • Vá em Microsoft Entra ID → App registrations → New registration.
  • Preencha os campos:

    • Name: nextjs-auth-app
    • Redirect URI: http://localhost:3000/api/auth/callback
    • Ative ID tokens e Access tokens.
  • Salve as credenciais:

    • Application (client) ID
    • Directory (tenant) ID
    • Client Secret (criado manualmente em Certificates & secrets).

2. Frontend Next.js com NextAuth

Instalação

npm install next-auth @azure/msal-node

Variáveis de ambiente

Arquivo .env.local:

AZURE_AD_CLIENT_ID=<Application ID>       # ID da aplicação registrada no Entra ID
AZURE_AD_CLIENT_SECRET=<Client Secret>    # Segredo do app (não expor no frontend)
AZURE_AD_TENANT_ID=<Directory ID>         # Tenant (diretório da organização)
NEXTAUTH_URL=http://localhost:3000        # URL da aplicação Next.js
NEXTAUTH_SECRET=<chave-aleatória>         # Chave para assinar JWT de sessão

Configuração do NextAuth

import NextAuth from "next-auth";
import AzureADProvider from "next-auth/providers/azure-ad";

// Configuração principal do NextAuth
export default NextAuth({
  providers: [
    // Habilita login com Microsoft Entra ID (Azure AD)
    AzureADProvider({
      clientId: process.env.AZURE_AD_CLIENT_ID!,        // ID do app registrado no Entra ID
      clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,// Segredo do cliente (guardado no backend)
      tenantId: process.env.AZURE_AD_TENANT_ID!,        // ID do diretório (tenant)
    }),
  ],
  session: { strategy: "jwt" }, // Sessões baseadas em JWT (sem persistência no BD)
  callbacks: {
    // Callback executado ao criar/atualizar JWT
    async jwt({ token, account }) {
      if (account) token.accessToken = account.access_token; // Armazena o accessToken no token
      return token;
    },
    // Callback executado ao montar a sessão
    async session({ session, token }) {
      session.accessToken = token.accessToken as string; // Inclui o accessToken na sessão
      return session;
    },
  },
});

Componente de Login/Logout (AuthButton.tsx)

"use client"; // Necessário porque usamos interações de browser (signIn/signOut)

import { signIn, signOut, useSession } from "next-auth/react";

export default function AuthButton() {
  const { data: session } = useSession(); // Hook que recupera a sessão atual

  if (session) {
    // Caso o usuário esteja logado
    return (
      <button onClick={() => signOut()}>
        Sair ({session.user?.name}) {/* Mostra o nome do usuário logado */}
      </button>
    );
  }
  // Caso não esteja logado
  return (
    <button onClick={() => signIn("azure-ad")}>
      Entrar com Entra ID {/* Chama fluxo de login no Entra ID */}
    </button>
  );
}

Tela Principal (page.tsx)

import AuthButton from "./components/AuthButton"; // Importa o botão de login/logout
import Link from "next/link";

export default function Home() {
  return (
    <main style={{ padding: "2rem" }}>
      <h1>🔐 Demo de Autenticação com Entra ID</h1>
      {/* Botão que mostra "Entrar" ou "Sair", dependendo da sessão */}
      <AuthButton />

      <hr style={{ margin: "2rem 0" }} />

      {/* Link para acessar página protegida */}
      <Link href="/protected">
        <button>Acessar página protegida</button>
      </Link>
    </main>
  );
}

3. Server Component: Página Protegida

import { authOptions } from "../api/auth/[...nextauth]/route";
import { getServerSession } from "next-auth";

export default async function ProtectedPage() {
  // Recupera a sessão no lado do servidor
  const session = await getServerSession(authOptions);

  if (!session) {
    // Caso o usuário não esteja autenticado
    return <p>Você precisa estar logado.</p>;
  }

  // Faz requisição ao backend protegido enviando o accessToken no header
  const res = await fetch("http://localhost:4000/api/secure-data", {
    headers: {
      Authorization: `Bearer ${session.accessToken}`, // Token nunca vai para o client
    },
    cache: "no-store", // Garante que não será cacheado
  });

  const data = await res.json();

  return (
    <div>
      <h2>Olá, {session.user?.name}</h2> {/* Nome do usuário logado */}
      <pre>{JSON.stringify(data, null, 2)}</pre> {/* Dados retornados da API */}
    </div>
  );
}

4. Backend Node/TypeScript

Dependências

npm install express jwks-rsa express-jwt

Código (src/index.ts)

import express from "express";
import { expressjwt as jwt } from "express-jwt";
import jwksRsa from "jwks-rsa";

const app = express();
const port = 4000;

// Middleware que valida o JWT recebido
const checkJwt = jwt({
  // Define a forma de obter as chaves públicas do Entra ID
  secret: jwksRsa.expressJwtSecret({
    cache: true,                      // Cache para não buscar chave toda vez
    rateLimit: true,                  // Limite de requisições para JWKS
    jwksRequestsPerMinute: 10,        // Máx. 10 req/min
    jwksUri: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}/discovery/v2.0/keys`,
  }) as any,
  audience: process.env.AZURE_AD_CLIENT_ID,               // Valida se o token é para este app
  issuer: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}/v2.0`, // Origem do token
  algorithms: ["RS256"],                                  // Algoritmo esperado
});

// Rota protegida: só acessa se o JWT for válido
app.get("/api/secure-data", checkJwt, (req, res) => {
  res.json({
    message: "Autenticado ✅",
    claims: (req as any).auth, // Retorna as claims do token decodificado
  });
});

// Inicializa o servidor
app.listen(port, () =>
  console.log(`🚀 Backend rodando em http://localhost:${port}`)
);

Boas Práticas

  • Proteção de secrets: nunca exponha clientSecret no frontend.
  • Sessões seguras: use cookies httpOnly ou JWT fornecidos pelo NextAuth.
  • Validação rigorosa: configure corretamente audience e issuer.
  • Observabilidade: registre logs de falhas de autenticação.
  • Escalabilidade: esse padrão permite múltiplos serviços validando tokens sem depender do frontend.

Conclusão

Com essa arquitetura, garantimos um fluxo seguro de ponta a ponta:

  • O Next.js gerencia autenticação e sessão do usuário.
  • O backend Node/TS valida tokens antes de liberar dados sensíveis.
  • O Microsoft Entra ID centraliza a identidade, oferecendo segurança corporativa.

Esse modelo é flexível e pode ser expandido para microsserviços ou integração com APIs da Microsoft (Graph API).


Referências