DEV Community

Cover image for Autenticação Segura com Microsoft Entra ID para desenvolvedores Typescript
Cláudio Filipe Lima Rapôso
Cláudio Filipe Lima Rapôso

Posted on

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 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Backend Node/TypeScript

Dependências

npm install express jwks-rsa express-jwt
Enter fullscreen mode Exit fullscreen mode

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}`)
);
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)