Introdução

Se você desenvolve aplicações web, APIs ou sistemas distribuídos, já se deparou com a necessidade de responder a duas perguntas fundamentais: “quem é você?” (autenticação) e “o que você pode fazer?” (autorização). Esses dois conceitos são os pilares de qualquer sistema seguro, mas frequentemente geram confusão — especialmente quando entram em cena protocolos como OAuth2, OpenID Connect e tokens como JWT.

Neste artigo, vamos descomplicar esses conceitos de forma prática e didática. Você vai entender a diferença entre autenticação e autorização, como o JWT (JSON Web Token) funciona por dentro, o papel do OAuth 2.0 na delegação de acesso, e como o OpenID Connect adiciona a camada de identidade que faltava. Além disso, vamos explorar os tipos de Client ID que existem no Azure Entra ID (antigo Azure AD) e quando usar cada um para aplicações SPA, Server-Side e API REST.

Este conteúdo é voltado para desenvolvedores back-end, front-end e full-stack que desejam implementar autenticação e autorização de forma correta e segura. Se você trabalha com automação e integração de sistemas, este artigo também será útil — assim como entender as diferenças entre terminal e pipeline é relevante para contextos de execução, a segurança dos seus fluxos depende de compreender como identidades são gerenciadas. E se você já domina esses fundamentos e quer elevar a segurança da sua SPA ao próximo nível, confira BFF Backend For Frontend: Segurança em SPAs, onde aplicamos esses conceitos no padrão BFF.


Autenticação vs. Autorização: Qual a Diferença?

Antes de mergulharmos nos protocolos, é essencial entender a diferença entre esses dois conceitos que, apesar de relacionados, têm responsabilidades completamente distintas.

Autenticação (Authentication — AuthN) é o processo de verificar a identidade de um usuário ou sistema. É a resposta para a pergunta: “Você é quem diz ser?”. Quando você digita seu e-mail e senha em uma tela de login, está passando por um processo de autenticação. Outros exemplos incluem biometria, certificados digitais e tokens de acesso.

Autorização (Authorization — AuthZ) é o processo de verificar permissões. Após confirmar quem você é, o sistema precisa decidir: “O que você tem permissão para fazer?”. Um usuário autenticado pode ter acesso a determinados recursos, mas não a todos. Por exemplo, um desenvolvedor pode ter permissão para ler logs de produção, mas não para excluir bancos de dados.

AspectoAutenticação (AuthN)Autorização (AuthZ)
Pergunta“Quem é você?”“O que você pode fazer?”
MomentoOcorre primeiroOcorre após a autenticação
MecanismoLogin, senhas, tokens, biometriaRoles, claims, policies, escopos
ProtocoloOpenID Connect, SAMLOAuth 2.0
ExemploFazer login com e-mail e senhaAcessar o endpoint /admin da API

ℹ️ Informação: Um erro muito comum é tratar OAuth 2.0 como um protocolo de autenticação. Na verdade, o OAuth 2.0 é um protocolo de autorização. Quem adiciona a camada de autenticação é o OpenID Connect, que é construído em cima do OAuth 2.0.


O que é JWT (JSON Web Token)?

O JWT (pronuncia-se “jot”) é um padrão aberto definido na RFC 7519 para transmitir informações de forma compacta e segura entre partes como um objeto JSON. Ele é amplamente utilizado como formato de token em fluxos de autenticação e autorização.

Estrutura de um JWT

Um JWT é composto por três partes separadas por pontos (.):

1
xxxxx.yyyyy.zzzzz
  1. Header — contém o tipo do token e o algoritmo de assinatura.
  2. Payload — contém as claims (declarações) sobre a entidade e metadados.
  3. Signature — garante que o token não foi alterado.

Exemplo de JWT decodificado

Header:

1
2
3
4
5
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "abc123"
}

Payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "iss": "https://login.microsoftonline.com/{tenant-id}/v2.0",
  "sub": "user-object-id",
  "aud": "api://minha-api-client-id",
  "exp": 1739750400,
  "iat": 1739746800,
  "name": "Lincoln Zocateli",
  "preferred_username": "[email protected]",
  "roles": ["Admin", "Reader"],
  "scp": "User.Read Profile.Read"
}

Claims importantes no contexto do Azure Entra ID

ClaimDescrição
issEmissor do token (issuer) — identifica o Azure Entra ID
subSubject — identificador único do usuário
audAudience — para qual aplicação/API o token foi emitido
expExpiração do token (timestamp Unix)
iatMomento da emissão (issued at)
rolesRoles atribuídas ao usuário na aplicação
scpEscopos delegados (scopes) — permissões concedidas
tidTenant ID do Azure Entra
oidObject ID do usuário no diretório

⚠️ Atenção: Nunca armazene informações sensíveis no payload de um JWT. O payload é codificado em Base64, mas não é criptografado — qualquer pessoa pode decodificá-lo. A assinatura garante apenas a integridade, não a confidencialidade. Se precisar de confidencialidade, utilize JWE (JSON Web Encryption) conforme a RFC 7516.

Validação de um JWT

Ao receber um JWT, sua aplicação DEVE validar:

  1. Assinatura — verificar com a chave pública do emissor (disponível no endpoint JWKS).
  2. Issuer (iss) — confirmar que o emissor é o esperado.
  3. Audience (aud) — confirmar que o token foi emitido para sua API.
  4. Expiração (exp) — rejeitar tokens expirados.
  5. Not Before (nbf) — se presente, rejeitar tokens antes do horário permitido.

OAuth 2.0: O Protocolo de Autorização

O OAuth 2.0 é um framework de autorização definido na RFC 6749 que permite que aplicações obtenham acesso limitado a recursos de um usuário em um servidor, sem expor as credenciais do usuário à aplicação.

Os Quatro Papéis do OAuth 2.0

O protocolo define quatro papéis fundamentais:

  1. Resource Owner — o usuário que possui os dados (ex: o dono da conta).
  2. Client — a aplicação que quer acessar os dados em nome do usuário.
  3. Authorization Server — o servidor que autentica o usuário e emite tokens (ex: Azure Entra ID).
  4. Resource Server — a API que protege os recursos e valida os tokens.

Principais Fluxos (Grant Types)

O OAuth 2.0 define diversos fluxos para diferentes cenários. Cada tipo de aplicação utiliza o fluxo mais adequado à sua arquitetura:

FluxoCenárioSegurança
Authorization Code + PKCESPAs, apps mobile, apps webAlta
Client CredentialsComunicação servidor-a-servidor (M2M)Alta
On-Behalf-Of (OBO)APIs chamando outras APIs em nome do userAlta
Device CodeDispositivos sem navegador (IoT, CLI)Média
ImplicitSPAs (OBSOLETO — não usar)Baixa
Resource Owner Password (ROPC)Login direto com senha (OBSOLETO)Baixa

⚠️ Atenção: Os fluxos Implicit e Resource Owner Password Credentials (ROPC) são considerados obsoletos e inseguros. A recomendação da IETF e da Microsoft é utilizar Authorization Code com PKCE para todas as aplicações públicas (SPAs e mobile).

O Fluxo Authorization Code com PKCE

Este é o fluxo recomendado para a maioria das aplicações modernas. Veja o passo a passo:

  1. O Client gera um code_verifier (string aleatória) e um code_challenge (hash SHA-256 do verifier).
  2. O Client redireciona o usuário para o Authorization Server com o code_challenge.
  3. O usuário se autentica e consente o acesso.
  4. O Authorization Server retorna um authorization code para o Client.
  5. O Client troca o authorization code + code_verifier por um access token.
  6. O Client usa o access token para acessar o Resource Server (API).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌──────────┐                              ┌───────────────────┐
│  Client   │──(1) Auth Request + PKCE ──►│ Authorization     │
│  (App)    │                              │ Server            │
│           │◄─(4) Authorization Code ─────│ (Azure Entra ID)  │
│           │──(5) Code + Verifier ───────►│                   │
│           │◄─(6) Access Token ──────────│                   │
│           │                              └───────────────────┘
│           │──(7) API Request + Token ──►┌───────────────────┐
│           │◄─(8) Protected Resource ────│ Resource Server   │
└──────────┘                              │ (Sua API)         │
                                          └───────────────────┘

OpenID Connect: Identidade sobre OAuth 2.0

O OpenID Connect (OIDC) é uma camada de identidade construída sobre o OAuth 2.0. Enquanto o OAuth 2.0 responde “o que você pode acessar?”, o OIDC responde “quem é você?”.

O OIDC introduz um novo tipo de token: o ID Token. Esse token é um JWT que contém informações sobre o usuário autenticado (nome, e-mail, foto, etc.) e é emitido junto com o access token.

Diferença entre os Tokens

TokenPropósitoFormatoQuem consome?
ID TokenInformações de identidade do usuárioJWTO Client (front-end)
Access TokenAutorização para acessar recursos protegidosJWT*O Resource Server
Refresh TokenObter novos access tokens sem reautenticarOpacoO Authorization Server

ℹ️ Informação: O Access Token emitido pelo Azure Entra ID é um JWT, mas a especificação do OAuth 2.0 não obriga esse formato — alguns provedores usam tokens opacos. Sempre valide o token conforme a documentação do seu provedor.

Escopos do OpenID Connect

O OIDC define escopos padronizados que determinam quais informações do usuário são retornadas:

  • openid — obrigatório para ativar o fluxo OIDC; retorna o claim sub.
  • profile — retorna name, family_name, given_name, picture, etc.
  • email — retorna email e email_verified.
  • offline_access — solicita um refresh token para renovação do acesso.

Tipos de Client ID no Azure Entra ID

No Azure Entra ID, ao registrar uma aplicação, você obtém um Client ID (também chamado de Application ID). Porém, o tipo de registro e a configuração da plataforma variam conforme a natureza da aplicação. Isso afeta diretamente o fluxo OAuth utilizado e o nível de segurança.

Aplicação Pública vs. Confidencial

O Azure Entra ID classifica as aplicações em dois tipos fundamentais:

TipoPode guardar segredo?ExemplosFluxos recomendados
PúblicaNãoSPA, mobile, desktop, CLIAuthorization Code + PKCE
ConfidencialSimWeb app server-side, daemon, APIAuth Code + Secret, Client Credentials

💡 Dica: Uma aplicação é pública quando executa em um ambiente que o usuário controla (navegador, celular, desktop) — onde não é possível armazenar um segredo com segurança. Uma aplicação é confidencial quando executa em um servidor que você controla.

1. Client ID para Aplicações SPA (Single Page Application)

Uma SPA (Single Page Application) é uma aplicação front-end que roda inteiramente no navegador, como aplicações React, Angular ou Vue.js. Como o código é executado no lado do cliente, não é possível armazenar Client Secrets — qualquer segredo seria exposto no código-fonte ou nas ferramentas de desenvolvedor do navegador.

Configuração no Azure Entra ID:

  • Tipo de plataforma: Single-page application
  • Tipo de aplicação: Public client
  • Redirect URI: http://localhost:4200 (dev) / https://app.seudominio.com (produção)
  • Fluxo OAuth: Authorization Code com PKCE
  • Client Secret: NÃO usar — aplicações públicas não devem ter secret

Registro via Azure CLI:

1
2
3
4
5
6
7
8
9
# Registrar uma aplicação SPA no Azure Entra ID
az ad app create \
  --display-name "Minha SPA Angular" \
  --sign-in-audience "AzureADMyOrg" \
  --enable-id-token-issuance true \
  --enable-access-token-issuance false \
  --web-redirect-uris "" \
  --public-client-redirect-uris "http://localhost:4200" \
  --is-fallback-public-client true

Exemplo com Angular 16+ e @azure/msal-angular:

Primeiro, instale as dependências necessárias:

1
npm install @azure/msal-browser @azure/msal-angular

Configuração do MSAL (auth-config.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// src/app/auth-config.ts — Configuração do MSAL para Angular 16+
import { MsalGuardConfiguration, MsalInterceptorConfiguration } from "@azure/msal-angular";
import {
  BrowserCacheLocation,
  InteractionType,
  IPublicClientApplication,
  LogLevel,
  PublicClientApplication,
} from "@azure/msal-browser";

// Configuração do MSAL — NÃO inclui client secret (app pública)
export function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication({
    auth: {
      clientId: "seu-client-id-aqui",             // Application (client) ID
      authority: "https://login.microsoftonline.com/seu-tenant-id",
      redirectUri: "http://localhost:4200",         // Redirect URI configurada no portal
      postLogoutRedirectUri: "http://localhost:4200",
    },
    cache: {
      cacheLocation: BrowserCacheLocation.SessionStorage, // sessionStorage para SPAs
      storeAuthStateInCookie: false,
    },
    system: {
      loggerOptions: {
        logLevel: LogLevel.Info,
        loggerCallback: (level, message) => console.log(message),
      },
    },
  });
}

// Configuração do MsalGuard — protege rotas que exigem autenticação
export function MSALGuardConfigFactory(): MsalGuardConfiguration {
  return {
    interactionType: InteractionType.Redirect, // Redirect ou Popup
    authRequest: {
      scopes: ["openid", "profile", "email"],
    },
  };
}

// Configuração do MsalInterceptor — injeta access token automaticamente nas requisições HTTP
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
  const protectedResources = new Map<string, Array<string>>();
  // Toda requisição para sua API receberá o token automaticamente
  protectedResources.set("https://api.seudominio.com/*", ["api://minha-api/User.Read"]);
  // Microsoft Graph (opcional)
  protectedResources.set("https://graph.microsoft.com/v1.0/*", ["User.Read"]);

  return {
    interactionType: InteractionType.Redirect,
    protectedResourceMap: protectedResources,
  };
}

Módulo principal com standalone API (app.config.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// src/app/app.config.ts — Configuração standalone do Angular 16+
import { ApplicationConfig, importProvidersFrom } from "@angular/core";
import { provideRouter } from "@angular/router";
import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from "@angular/common/http";
import {
  MsalModule,
  MsalService,
  MsalGuard,
  MsalInterceptor,
  MsalBroadcastService,
  MSAL_INSTANCE,
  MSAL_GUARD_CONFIG,
  MSAL_INTERCEPTOR_CONFIG,
} from "@azure/msal-angular";
import {
  MSALInstanceFactory,
  MSALGuardConfigFactory,
  MSALInterceptorConfigFactory,
} from "./auth-config";
import { routes } from "./app.routes";

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptorsFromDi()),
    importProvidersFrom(MsalModule),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor, // Interceptor injeta token nas requisições automaticamente
      multi: true,
    },
    {
      provide: MSAL_INSTANCE,
      useFactory: MSALInstanceFactory,
    },
    {
      provide: MSAL_GUARD_CONFIG,
      useFactory: MSALGuardConfigFactory,
    },
    {
      provide: MSAL_INTERCEPTOR_CONFIG,
      useFactory: MSALInterceptorConfigFactory,
    },
    MsalService,
    MsalGuard,             // Guard para proteger rotas
    MsalBroadcastService,  // Service para observar eventos de autenticação
  ],
};

Rotas protegidas (app.routes.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/app/app.routes.ts — Rotas com MsalGuard para proteger acesso
import { Routes } from "@angular/router";
import { MsalGuard } from "@azure/msal-angular";
import { HomeComponent } from "./home/home.component";
import { DashboardComponent } from "./dashboard/dashboard.component";

export const routes: Routes = [
  { path: "", component: HomeComponent },
  {
    path: "dashboard",
    component: DashboardComponent,
    canActivate: [MsalGuard], // Rota protegida — requer autenticação
  },
];

Componente com login e chamada à API (home.component.ts):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// src/app/home/home.component.ts — Componente com login e consumo de API
import { Component, OnInit, OnDestroy, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { HttpClient } from "@angular/common/http";
import { MsalService, MsalBroadcastService } from "@azure/msal-angular";
import { InteractionStatus } from "@azure/msal-browser";
import { Subject, filter, takeUntil } from "rxjs";

@Component({
  selector: "app-home",
  standalone: true,
  imports: [CommonModule],
  template: `
    <div *ngIf="!isAutenticado">
      <h2>Bem-vindo</h2>
      <button (click)="login()">Login com Azure Entra ID</button>
    </div>
    <div *ngIf="isAutenticado">
      <h2>Olá, {{ nomeUsuario }}!</h2>
      <button (click)="chamarApi()">Chamar API Protegida</button>
      <button (click)="logout()">Logout</button>
      <pre *ngIf="dadosApi">{{ dadosApi | json }}</pre>
    </div>
  `,
})
export class HomeComponent implements OnInit, OnDestroy {
  private msalService = inject(MsalService);
  private broadcastService = inject(MsalBroadcastService);
  private http = inject(HttpClient);
  private destroy$ = new Subject<void>();

  isAutenticado = false;
  nomeUsuario = "";
  dadosApi: any = null;

  ngOnInit(): void {
    // Observar mudanças no estado de autenticação
    this.broadcastService.inProgress$
      .pipe(
        filter((status) => status === InteractionStatus.None),
        takeUntil(this.destroy$)
      )
      .subscribe(() => {
        const accounts = this.msalService.instance.getAllAccounts();
        this.isAutenticado = accounts.length > 0;
        if (this.isAutenticado) {
          this.nomeUsuario = accounts[0].name ?? accounts[0].username;
        }
      });
  }

  login(): void {
    // Inicia o fluxo de login via redirect (Authorization Code + PKCE)
    this.msalService.loginRedirect({
      scopes: ["openid", "profile", "email", "api://minha-api/User.Read"],
    });
  }

  logout(): void {
    this.msalService.logoutRedirect({
      postLogoutRedirectUri: "http://localhost:4200",
    });
  }

  chamarApi(): void {
    // O MsalInterceptor injeta o access token automaticamente
    this.http.get("https://api.seudominio.com/api/dados").subscribe({
      next: (data) => (this.dadosApi = data),
      error: (err) => console.error("Erro ao chamar API:", err),
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

💡 Dica: No Angular 16+, o MsalInterceptor injeta automaticamente o access token em todas as requisições HTTP que correspondam aos padrões configurados no protectedResourceMap. Você não precisa gerenciar tokens manualmente — basta usar o HttpClient normalmente.

ℹ️ Informação: Embora o MSAL funcione bem para SPAs, a abordagem mais segura atualmente é o padrão BFF (Backend For Frontend), que evita expor tokens no navegador. Veja a implementação completa em BFF Backend For Frontend: Segurança em SPAs.

2. Client ID para Aplicações Server-Side (Web App)

Aplicações server-side são aplicações web tradicionais que executam no servidor (ex: ASP.NET, Django, Express.js). Como o código roda em um ambiente controlado, essas aplicações podem armazenar um Client Secret com segurança.

Configuração no Azure Entra ID:

  • Tipo de plataforma: Web
  • Tipo de aplicação: Confidential client
  • Redirect URI: https://app.seudominio.com/auth/callback
  • Fluxo OAuth: Authorization Code (com Client Secret)
  • Client Secret: Sim — armazenado de forma segura no servidor (ex: variável de ambiente, Key Vault)

Registro via Azure CLI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Registrar uma aplicação web confidencial
az ad app create \
  --display-name "Minha Web App" \
  --sign-in-audience "AzureADMyOrg" \
  --web-redirect-uris "https://app.seudominio.com/auth/callback" \
  --enable-id-token-issuance true

# Criar um Client Secret (válido por 2 anos)
az ad app credential reset \
  --id "seu-app-object-id" \
  --display-name "Prod Secret" \
  --years 2

Exemplo com Python (Flask + MSAL):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# app.py — Aplicação Flask com autenticação via Azure Entra ID
import os
from flask import Flask, redirect, url_for, session, request
import msal

app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-secret-key")

# Configuração — Client Secret armazenado em variável de ambiente
CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"]  # Seguro no servidor
AUTHORITY = f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}"
REDIRECT_URI = "https://app.seudominio.com/auth/callback"
SCOPE = ["User.Read"]  # Escopos do Microsoft Graph

# Criar instância confidencial do MSAL
def criar_msal_app():
    return msal.ConfidentialClientApplication(
        CLIENT_ID,
        authority=AUTHORITY,
        client_credential=CLIENT_SECRET,  # Secret usado aqui (servidor seguro)
    )

@app.route("/login")
def login():
    """Inicia o fluxo de autenticação OAuth 2.0 Authorization Code."""
    msal_app = criar_msal_app()
    # Gerar URL de autorização
    auth_url = msal_app.get_authorization_request_url(
        scopes=SCOPE,
        redirect_uri=REDIRECT_URI,
    )
    return redirect(auth_url)

@app.route("/auth/callback")
def callback():
    """Callback do Azure Entra ID após autenticação."""
    msal_app = criar_msal_app()
    code = request.args.get("code")

    # Trocar authorization code por tokens
    result = msal_app.acquire_token_by_authorization_code(
        code,
        scopes=SCOPE,
        redirect_uri=REDIRECT_URI,
    )

    if "access_token" in result:
        session["user"] = result.get("id_token_claims")
        session["access_token"] = result["access_token"]
        return redirect(url_for("index"))
    else:
        return f"Erro na autenticação: {result.get('error_description')}", 401

@app.route("/")
def index():
    """Página principal — requer autenticação."""
    user = session.get("user")
    if not user:
        return redirect(url_for("login"))
    return f"Olá, {user.get('name')}! Você está autenticado."

if __name__ == "__main__":
    app.run(debug=True, port=5000)

⚠️ Atenção: Nunca exponha o Client Secret no código-fonte, repositórios Git ou logs. Utilize variáveis de ambiente, Azure Key Vault ou ferramentas de gerenciamento de segredos. No exemplo acima, os valores vêm de os.environ por segurança.

3. Client ID para APIs REST (Comunicação Machine-to-Machine)

Quando uma API backend ou serviço daemon precisa acessar outra API protegida sem interação de um usuário, utiliza-se o fluxo Client Credentials. Neste cenário, a própria aplicação se autentica com seu Client ID e Client Secret (ou certificado), sem envolver nenhum usuário.

Configuração no Azure Entra ID:

  • Tipo de plataforma: Nenhuma (não precisa de Redirect URI)
  • Tipo de aplicação: Confidential client
  • Fluxo OAuth: Client Credentials
  • Client Secret ou Certificado: Sim — obrigatório
  • Permissões: Application permissions (não delegadas)

Cenários comuns:

  • Cronjobs e serviços agendados que consomem APIs
  • Microsserviço A chamando Microsserviço B
  • Pipelines de CI/CD que acessam recursos protegidos
  • Daemons e workers em background

Exemplo com Python (requests + MSAL):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# daemon_service.py — Serviço M2M com Client Credentials
import os
import msal
import requests

# Configuração do daemon/serviço
CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"]
TENANT_ID = os.environ["AZURE_TENANT_ID"]
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"

# Escopo da API alvo — formato: api://<client-id>/.default
API_SCOPE = ["api://api-destino-client-id/.default"]
API_BASE_URL = "https://api.seudominio.com"

def obter_token_m2m():
    """Obtém access token usando Client Credentials (sem usuário)."""
    app = msal.ConfidentialClientApplication(
        CLIENT_ID,
        authority=AUTHORITY,
        client_credential=CLIENT_SECRET,
    )

    # Tenta usar token em cache antes de solicitar novo
    result = app.acquire_token_silent(API_SCOPE, account=None)
    if not result:
        # Solicita novo token via Client Credentials
        result = app.acquire_token_for_client(scopes=API_SCOPE)

    if "access_token" in result:
        return result["access_token"]
    else:
        raise Exception(f"Falha ao obter token: {result.get('error_description')}")

def chamar_api_protegida():
    """Chama a API protegida usando o access token M2M."""
    token = obter_token_m2m()

    # Incluir o token no header Authorization
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }

    response = requests.get(f"{API_BASE_URL}/api/dados", headers=headers)
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    dados = chamar_api_protegida()
    print(f"Dados recebidos da API: {dados}")

💡 Dica: Para cenários de produção com alta segurança, prefira certificados em vez de Client Secrets para autenticação de serviços M2M. Certificados oferecem maior segurança e podem ser gerenciados via Azure Key Vault.


Tabela Comparativa: Qual Tipo de Client ID Usar?

Esta tabela resume as decisões que você precisa tomar ao registrar sua aplicação no Azure Entra ID:

CaracterísticaSPA (Public Client)Server-Side (Confidential)API REST / Daemon (Confidential)
Onde executaNavegador do usuárioServidor webServidor backend
Client SecretNãoSimSim (ou certificado)
Fluxo OAuthAuth Code + PKCEAuth Code + SecretClient Credentials
Interação do usuárioSim (login interativo)Sim (login interativo)Não (M2M)
Tokens recebidosID Token + Access TokenID Token + Access TokenApenas Access Token
Refresh TokenSim (com offline_access)SimNão (usa cache de token)
Exemplo de appAngular, React, VueFlask, ASP.NET, ExpressCronjob, microsserviço, daemon
Plataforma no portalSingle-page applicationWebNenhuma (só permissões de app)
Biblioteca MSAL@azure/msal-angularmsal-python / MSAL.NETmsal-python / MSAL.NET

Protegendo Sua API com C#: Validação de Tokens no ASP.NET Core

Independentemente de qual tipo de client chama sua API, a API REST precisa validar o access token recebido. Veja como fazer isso em C# com ASP.NET Core usando o pacote Microsoft.Identity.Web, que é a forma recomendada pela Microsoft para proteger APIs com Azure Entra ID.

Primeiro, crie o projeto e instale as dependências:

1
2
3
4
5
6
7
# Criar projeto Web API
dotnet new webapi -n MinhaApiProtegida --framework net8.0
cd MinhaApiProtegida

# Instalar pacotes de autenticação Microsoft Identity
dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.Identity.Web.MicrosoftGraph  # Opcional: se precisar chamar Graph

Configuração (appsettings.json):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "seu-tenant-id",
    "ClientId": "api://seu-client-id-da-api",
    "Audience": "api://seu-client-id-da-api",
    "Scopes": "User.Read Admin.Full"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

Configuração do Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Program.cs — API REST protegida com Azure Entra ID (ASP.NET Core 8)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

// Configura autenticação JWT Bearer com Azure Entra ID
// O Microsoft.Identity.Web cuida automaticamente de:
// - Buscar chaves públicas (JWKS) do Azure Entra ID
// - Validar issuer, audience, assinatura e expiração do token
// - Validar escopos e roles
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

// Configurar políticas de autorização baseadas em roles e escopos
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("LeituraPermitida", policy =>
        policy.RequireScope("User.Read"))       // Exige escopo User.Read
    .AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));            // Exige role Admin

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configurar CORS para permitir requisições da SPA Angular
builder.Services.AddCors(options =>
{
    options.AddPolicy("PermitirSPA", corsBuilder =>
    {
        corsBuilder
            .WithOrigins("http://localhost:4200", "https://app.seudominio.com")
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("PermitirSPA");
app.UseAuthentication();  // Middleware de autenticação (valida o JWT)
app.UseAuthorization();   // Middleware de autorização (verifica policies)
app.MapControllers();

app.Run();

Controller com endpoints protegidos (DadosController.cs):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Controllers/DadosController.cs — Endpoints protegidos com autorização granular
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;

namespace MinhaApiProtegida.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize]  // Todos os endpoints exigem autenticação por padrão
public class DadosController : ControllerBase
{
    /// <summary>
    /// Endpoint protegido — requer access token válido com escopo User.Read.
    /// Pode ser chamado por SPAs (delegated) ou daemons (application).
    /// </summary>
    [HttpGet]
    [RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")] // Valida escopo
    public IActionResult ObterDados()
    {
        // Extrair informações do token JWT validado
        var usuario = User.GetDisplayName()
                     ?? User.FindFirst("preferred_username")?.Value
                     ?? "Desconhecido";

        var objectId = User.GetObjectId();             // OID do Azure Entra
        var tenantId = User.GetTenantId();             // Tenant ID
        var scopes = User.FindFirst("scp")?.Value;     // Escopos delegados

        return Ok(new
        {
            Mensagem = "Acesso autorizado!",
            Usuario = usuario,
            ObjectId = objectId,
            TenantId = tenantId,
            Scopes = scopes,
            Timestamp = DateTime.UtcNow
        });
    }

    /// <summary>
    /// Endpoint admin — requer role 'Admin' atribuída no Azure Entra ID.
    /// </summary>
    [HttpGet("admin")]
    [Authorize(Policy = "AdminOnly")] // Exige role Admin
    public IActionResult ObterDadosAdmin()
    {
        var roles = User.Claims
            .Where(c => c.Type == "roles" || c.Type == System.Security.Claims.ClaimTypes.Role)
            .Select(c => c.Value)
            .ToList();

        return Ok(new
        {
            Mensagem = "Bem-vindo, administrador!",
            Roles = roles,
            Usuario = User.GetDisplayName()
        });
    }

    /// <summary>
    /// Endpoint que diferencia chamadas delegadas (usuário) de chamadas M2M (daemon).
    /// Útil quando a mesma API é consumida por SPAs e por serviços backend.
    /// </summary>
    [HttpGet("info")]
    public IActionResult InformacoesToken()
    {
        var scopeClaim = User.FindFirst("scp");        // Presente em tokens delegados
        var rolesClaim = User.FindFirst("roles");      // Presente em tokens de aplicação

        var tipoFluxo = scopeClaim != null
            ? "Delegado (usuário via SPA/Web App)"
            : "Aplicação (M2M via Client Credentials)";

        return Ok(new
        {
            TipoFluxo = tipoFluxo,
            Issuer = User.FindFirst("iss")?.Value,
            Audience = User.FindFirst("aud")?.Value,
            Expiracao = User.FindFirst("exp")?.Value,
            Scopes = scopeClaim?.Value,
            Roles = rolesClaim?.Value
        });
    }
}

💡 Dica: O pacote Microsoft.Identity.Web simplifica enormemente a validação de tokens no ASP.NET Core. Ele busca automaticamente as chaves JWKS do Azure Entra ID, valida issuer, audience, assinatura e expiração — tudo configurado com apenas uma linha no Program.cs. Além disso, os extension methods como User.GetDisplayName() e User.GetObjectId() facilitam a extração de claims do JWT já validado.


Dicas e Boas Práticas

  • Sempre use PKCE para aplicações públicas: mesmo que o framework não exija, o PKCE protege contra ataques de interceptação de authorization code. O Azure Entra ID suporta PKCE para todos os fluxos.

  • Prefira certificados a Client Secrets para produção: Client Secrets expiram e podem ser vazados em logs. Certificados gerenciados pelo Azure Key Vault são mais seguros e com rotação automatizada.

  • Defina escopos granulares na sua API: em vez de um único escopo genérico, crie escopos como User.Read, User.Write, Admin.Full para aplicar o princípio do menor privilégio.

  • Valide TODOS os campos do JWT na sua API: não confie apenas na assinatura. Valide iss, aud, exp, nbf e os scopes/roles necessários para cada endpoint.

  • Use Managed Identities quando possível: para serviços rodando no Azure (App Service, Functions, VMs), use Managed Identities em vez de Client Secrets. Isso elimina a necessidade de gerenciar credenciais.

  • Configure tempos de expiração adequados: Access Tokens devem ter vida curta (padrão: 1 hora no Azure Entra). Use Refresh Tokens para renovação. Nunca estenda o tempo de expiração de forma exagerada.

  • Implemente logout corretamente: além de limpar tokens localmente, redirecione o usuário para o endpoint de logout do Azure Entra (/oauth2/v2.0/logout) para encerrar a sessão no provedor de identidade.

⚠️ Atenção: Nunca valide tokens apenas no front-end. A validação real deve ocorrer na API (Resource Server). O front-end pode inspecionar o ID Token para exibir informações do usuário, mas a autorização sempre acontece no backend.


Conclusão

Compreender a diferença entre autenticação e autorização é o fundamento para construir sistemas seguros. O JWT oferece um formato compacto e autocontido para transportar claims de identidade e permissões. O OAuth 2.0 resolve o problema da delegação de acesso sem expor credenciais, enquanto o OpenID Connect adiciona a camada de identidade que permite saber quem é o usuário autenticado.

Na prática, ao usar o Azure Entra ID, a escolha do tipo correto de Client ID é determinante para a segurança da sua aplicação: aplicações SPA usam clientes públicos com PKCE, aplicações server-side usam clientes confidenciais com secrets, e APIs/daemons usam o fluxo Client Credentials para comunicação machine-to-machine.

O mais importante é seguir o princípio de que cada aplicação deve ter apenas as permissões necessárias para executar sua função. Para SPAs que lidam com dados sensíveis, considere adotar o padrão BFF (Backend For Frontend), que move os tokens para o servidor — veja como implementar em BFF Backend For Frontend: Segurança em SPAs. Se você gostou deste conteúdo, deixe um comentário com suas dúvidas ou compartilhe com outros desenvolvedores. Para aprofundar seus conhecimentos em automação de projetos, confira também Makefile: Automatizando tarefas para Python, Hugo e Docker.


Leia Também


Referências