Udostępnij przez


Scenariusz: weryfikowanie nagłówka autoryzacji

Zweryfikuj przychodzące tokeny uwierzytelniające, przekazując je do punktu końcowego /Validate zestawu Microsoft Entra SDK dla AgentID, a następnie wyodrębnij zwrócone oświadczenia w celu podejmowania decyzji autoryzacyjnych. W tym przewodniku przedstawiono sposób implementowania warstwy pośredniczącej do weryfikacji tokenu i podejmowania decyzji dotyczących uprawnień na podstawie zakresów lub ról.

Wymagania wstępne

  • Konto Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
  • Zestaw Microsoft Entra SDK dla identyfikatora AgentID wdrożony i uruchomiony z dostępem do sieci z aplikacji. Aby uzyskać instrukcje dotyczące instalacji, zobacz Przewodnik instalacji.
  • Zarejestrowana aplikacja w usłudze Microsoft Entra ID — zarejestruj nową aplikację w centrum administracyjnym firmy Microsoft Entra, skonfigurowaną tylko dla kont w tym katalogu organizacyjnym. Aby uzyskać więcej informacji, zobacz Rejestrowanie aplikacji . Zapisz następujące wartości na stronie Przegląd aplikacji:
    • Identyfikator aplikacji (klienta)
    • Identyfikator katalogu (klienta)
    • Skonfiguruj identyfikator URI aplikacji w sekcji Udostępnienie interfejsu API (używany jako widownia do weryfikacji tokenów)
  • Tokeny dostępu typu Bearer od uwierzytelnionych klientów — aplikacja musi odbierać tokeny z aplikacji klienckich za pośrednictwem przepływów protokołu OAuth 2.0.
  • Odpowiednie uprawnienia w usłudze Microsoft Entra ID — Twoje konto musi mieć uprawnienia do rejestrowania aplikacji i konfigurowania ustawień uwierzytelniania.

Konfiguracja

Aby zweryfikować tokeny dla interfejsu API, skonfiguruj zestaw Microsoft Entra SDK for AgentID przy użyciu danych o dzierżawie Microsoft Entra ID.

env:
- name: AzureAd__Instance
  value: "https://login.microsoftonline.com/"
- name: AzureAd__TenantId
  value: "your-tenant-id"
- name: AzureAd__ClientId
  value: "your-api-client-id"
- name: AzureAd__Audience
  value: "api://your-api-id"

TypeScript/Node.js

Poniższa implementacja pokazuje, jak utworzyć oprogramowanie pośredniczące weryfikacji tokenu zintegrowane z zestawem Microsoft Entra SDK dla identyfikatora AgentID przy użyciu języka TypeScript lub JavaScript. To oprogramowanie pośredniczące sprawdza każde przychodzące żądanie prawidłowego tokenu elementu nośnego i wyodrębnia oświadczenia do użycia w programach obsługi tras:

import fetch from 'node-fetch';

interface ValidateResponse {
  protocol: string;
  token: string;
  claims: {
    aud: string;
    iss: string;
    oid: string;
    sub: string;
    tid: string;
    upn?: string;
    scp?: string;
    roles?: string[];
    [key: string]: any;
  };
}

async function validateToken(authorizationHeader: string): Promise<ValidateResponse> {
  const sidecarUrl = process.env.SIDECAR_URL || 'http://localhost:5000';
  
  const response = await fetch(`${sidecarUrl}/Validate`, {
    headers: {
      'Authorization': authorizationHeader
    }
  });
  
  if (!response.ok) {
    throw new Error(`Token validation failed: ${response.statusText}`);
  }
  
  return await response.json() as ValidateResponse;
}

Poniższy fragment kodu przedstawia sposób używania funkcji w oprogramowaniu pośredniczącym Express.js w celu ochrony punktów końcowych interfejsu API.

// Express.js middleware example
import express from 'express';

const app = express();

// Token validation middleware
async function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).json({ error: 'No authorization token provided' });
  }
  
  try {
    const validation = await validateToken(authHeader);
    
    // Attach claims to request object
    req.user = {
      id: validation.claims.oid,
      upn: validation.claims.upn,
      tenantId: validation.claims.tid,
      scopes: validation.claims.scp?.split(' ') || [],
      roles: validation.claims.roles || [],
      claims: validation.claims
    };
    
    next();
  } catch (error) {
    console.error('Token validation failed:', error);
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Protected endpoint
app.get('/api/protected', requireAuth, (req, res) => {
  res.json({
    message: 'Access granted',
    user: {
      id: req.user.id,
      upn: req.user.upn
    }
  });
});

// Scope-based authorization
app.get('/api/admin', requireAuth, (req, res) => {
  if (!req.user.roles.includes('Admin')) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }
  
  res.json({ message: 'Admin access granted' });
});

app.listen(8080);

Python

Poniższy fragment kodu języka Python używa dekoratorów platformy Flask do zawijania procedur obsługi tras przy użyciu weryfikacji tokenu. Ten dekorator wyodrębnia token typu bearer z nagłówka autoryzacji, weryfikuje go za pomocą Microsoft Entra SDK dla AgentID i udostępnia atrybuty na trasie:

import os
import requests
from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)

def validate_token(authorization_header: str) -> dict:
    """Validate token using the SDK."""
    sidecar_url = os.getenv('SIDECAR_URL', 'http://localhost:5000')
    
    response = requests.get(
        f"{sidecar_url}/Validate",
        headers={'Authorization': authorization_header}
    )
    
    if not response.ok:
        raise Exception(f"Token validation failed: {response.text}")
    
    return response.json()

# Token validation decorator
def require_auth(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        auth_header = request.headers.get('Authorization')
        
        if not auth_header:
            return jsonify({'error': 'No authorization token provided'}), 401
        
        try:
            validation = validate_token(auth_header)
            
            # Attach user info to Flask's g object
            from flask import g
            g.user = {
                'id': validation['claims']['oid'],
                'upn': validation['claims'].get('upn'),
                'tenant_id': validation['claims']['tid'],
                'scopes': validation['claims'].get('scp', '').split(' '),
                'roles': validation['claims'].get('roles', []),
                'claims': validation['claims']
            }
            
            return f(*args, **kwargs)
        except Exception as e:
            print(f"Token validation failed: {e}")
            return jsonify({'error': 'Invalid token'}), 401
    
    return decorated_function

# Protected endpoint
@app.route('/api/protected')
@require_auth
def protected():
    from flask import g
    return jsonify({
        'message': 'Access granted',
        'user': {
            'id': g.user['id'],
            'upn': g.user['upn']
        }
    })

# Role-based authorization
@app.route('/api/admin')
@require_auth
def admin():
    from flask import g
    if 'Admin' not in g.user['roles']:
        return jsonify({'error': 'Insufficient permissions'}), 403
    
    return jsonify({'message': 'Admin access granted'})

if __name__ == '__main__':
    app.run(port=8080)

Go

Poniższa implementacja języka Go demonstruje walidację tokenu przy użyciu standardowego wzorca procedury obsługi HTTP. To podejście oprogramowania pośredniczącego wyodrębnia tokeny elementu nośnego z nagłówka autoryzacji, weryfikuje je za pomocą zestawu Microsoft Entra SDK dla identyfikatora AgentID i przechowuje informacje o użytkowniku w nagłówkach żądań do użycia w programach obsługi podrzędnej:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "strings"
)

type ValidateResponse struct {
    Protocol string                 `json:"protocol"`
    Token    string                 `json:"token"`
    Claims   map[string]interface{} `json:"claims"`
}

type User struct {
    ID       string
    UPN      string
    TenantID string
    Scopes   []string
    Roles    []string
    Claims   map[string]interface{}
}

func validateToken(authHeader string) (*ValidateResponse, error) {
    sidecarURL := os.Getenv("SIDECAR_URL")
    if sidecarURL == "" {
        sidecarURL = "http://localhost:5000"
    }
    
    req, err := http.NewRequest("GET", fmt.Sprintf("%s/Validate", sidecarURL), nil)
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("Authorization", authHeader)
    
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("token validation failed: %s", resp.Status)
    }
    
    var validation ValidateResponse
    if err := json.NewDecoder(resp.Body).Decode(&validation); err != nil {
        return nil, err
    }
    
    return &validation, nil
}

// Middleware for token validation
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        
        if authHeader == "" {
            http.Error(w, "No authorization token provided", http.StatusUnauthorized)
            return
        }
        
        validation, err := validateToken(authHeader)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        // Extract user information from claims
        user := &User{
            ID:       validation.Claims["oid"].(string),
            TenantID: validation.Claims["tid"].(string),
            Claims:   validation.Claims,
        }
        
        if upn, ok := validation.Claims["upn"].(string); ok {
            user.UPN = upn
        }
        
        if scp, ok := validation.Claims["scp"].(string); ok {
            user.Scopes = strings.Split(scp, " ")
        }
        
        if roles, ok := validation.Claims["roles"].([]interface{}); ok {
            for _, role := range roles {
                user.Roles = append(user.Roles, role.(string))
            }
        }
        
        // Store user in context (simplified - use context.Context in production)
        r.Header.Set("X-User-ID", user.ID)
        r.Header.Set("X-User-UPN", user.UPN)
        
        next(w, r)
    }
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "message": "Access granted",
        "user": map[string]string{
            "id":  r.Header.Get("X-User-ID"),
            "upn": r.Header.Get("X-User-UPN"),
        },
    })
}

func main() {
    http.HandleFunc("/api/protected", requireAuth(protectedHandler))
    
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

C#

Poniższa implementacja języka C# demonstruje walidację tokenu przy użyciu oprogramowania pośredniczącego ASP.NET Core. Takie podejście używa iniekcji zależności w celu uzyskania dostępu do usługi weryfikacji tokenu, wyodrębnia tokeny typu bearer z nagłówka Authorization, weryfikuje je za pomocą Microsoft Entra SDK for AgentID i przechowuje żądania użytkowników w obiekcie HttpContext do użycia w kontrolerach.

using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;

public class ValidateResponse
{
    public string Protocol { get; set; }
    public string Token { get; set; }
    public JsonElement Claims { get; set; }
}

public class TokenValidationService
{
    private readonly HttpClient _httpClient;
    private readonly string _sidecarUrl;
    
    public TokenValidationService(IHttpClientFactory httpClientFactory, IConfiguration config)
    {
        _httpClient = httpClientFactory.CreateClient();
        _sidecarUrl = config["SIDECAR_URL"] ?? "http://localhost:5000";
    }
    
    public async Task<ValidateResponse> ValidateTokenAsync(string authorizationHeader)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"{_sidecarUrl}/Validate");
        request.Headers.Add("Authorization", authorizationHeader);
        
        var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
        
        return await response.Content.ReadFromJsonAsync<ValidateResponse>();
    }
}

// Middleware example
public class TokenValidationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly TokenValidationService _validationService;
    
    public TokenValidationMiddleware(RequestDelegate next, TokenValidationService validationService)
    {
        _next = next;
        _validationService = validationService;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        var authHeader = context.Request.Headers["Authorization"].ToString();
        
        if (string.IsNullOrEmpty(authHeader))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "No authorization token" });
            return;
        }
        
        try
        {
            var validation = await _validationService.ValidateTokenAsync(authHeader);
            
            // Store claims in HttpContext.Items for use in controllers
            context.Items["UserClaims"] = validation.Claims;
            context.Items["UserId"] = validation.Claims.GetProperty("oid").GetString();
            
            await _next(context);
        }
        catch (Exception ex)
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "Invalid token" });
        }
    }
}

// Controller example
[ApiController]
[Route("api")]
public class ProtectedController : ControllerBase
{
    [HttpGet("protected")]
    public IActionResult GetProtected()
    {
        var userId = HttpContext.Items["UserId"] as string;
        
        return Ok(new
        {
            message = "Access granted",
            user = new { id = userId }
        });
    }
}

Wyodrębnianie określonych roszczeń

Po zweryfikowaniu tokenu możesz wyodrębnić oświadczenia, aby podejmować decyzje dotyczące autoryzacji w aplikacji. Punkt /Validate końcowy zwraca obiekt oświadczeń z następującymi informacjami:

{
  "protocol": "Bearer",
  "claims": {
    "oid": "user-object-id",
    "upn": "user@contoso.com",
    "tid": "tenant-id",
    "scp": "User.Read Mail.Read",
    "roles": ["Admin"]
  }
}

Typowe oświadczenia obejmują:

  • oid: identyfikator obiektu (unikatowy identyfikator użytkownika) w dzierżawie Microsoft Entra ID
  • upn: główna nazwa użytkownika (zazwyczaj format poczty e-mail)
  • tid: identyfikator dzierżawcy, do którego należy użytkownik
  • scp: Delegowane zakresy, które użytkownik przyznał aplikacji
  • roles: Role aplikacji przypisane do użytkownika

W poniższych przykładach pokazano, jak wyodrębnić określone oświadczenia z odpowiedzi weryfikacji:

Tożsamość użytkownika:

// Extract user identity
const userId = validation.claims.oid;  // Object ID
const userPrincipalName = validation.claims.upn;  // User Principal Name
const tenantId = validation.claims.tid;  // Tenant ID

Zakresy i role:

// Extract scopes (delegated permissions)
const scopes = validation.claims.scp?.split(' ') || [];

// Check for specific scope
if (scopes.includes('User.Read')) {
  // Allow access
}

// Extract roles (application permissions)
const roles = validation.claims.roles || [];

// Check for specific role
if (roles.includes('Admin')) {
  // Allow admin access
}

Wzorce autoryzacji

Po zweryfikowaniu tokenów można wymusić autoryzację na podstawie delegowanych zakresów (uprawnień przyznanych przez użytkownika) lub ról aplikacji (przypisanych przez administratora dzierżawy). Wybierz wzorzec zgodny z modelem autoryzacji:

Autoryzacja oparta na zakresie

Przed udzieleniem dostępu sprawdź, czy token użytkownika zawiera wymagane zakresy:

function requireScopes(requiredScopes: string[]) {
  return async (req, res, next) => {
    const validation = await validateToken(req.headers.authorization);
    const userScopes = validation.claims.scp?.split(' ') || [];
    const hasAllScopes = requiredScopes.every(s => userScopes.includes(s));
    
    if (!hasAllScopes) {
      return res.status(403).json({ error: 'Insufficient scopes' });
    }
    next();
  };
}

app.get('/api/mail', requireScopes(['Mail.Read']), (req, res) => {
  res.json({ message: 'Mail access granted' });
});

Autoryzacja oparta na rolach

Sprawdź, czy użytkownik ma wymagane role aplikacji:

function requireRoles(requiredRoles: string[]) {
  return async (req, res, next) => {
    const validation = await validateToken(req.headers.authorization);
    const userRoles = validation.claims.roles || [];
    const hasRole = requiredRoles.some(r => userRoles.includes(r));
    
    if (!hasRole) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

app.delete('/api/resource', requireRoles(['Admin']), (req, res) => {
  res.json({ message: 'Resource deleted' });
});

Obsługa błędów

Weryfikacja tokenu może zakończyć się niepowodzeniem z kilku powodów: token może być wygasły, nieprawidłowy lub brakuje wymaganych zakresów. Zaimplementuj obsługę błędów, która rozróżnia różne scenariusze awarii, aby odpowiednio reagować:

async function validateTokenSafely(authHeader: string): Promise<ValidateResponse | null> {
  try {
    return await validateToken(authHeader);
  } catch (error) {
    if (error.message.includes('401')) {
      console.error('Token is invalid or expired');
    } else if (error.message.includes('403')) {
      console.error('Token missing required scopes');
    } else {
      console.error('Token validation error:', error.message);
    }
    return null;
  }
}

Typowe błędy walidacji

Error Przyczyna Rozwiązanie
401 Brak autoryzacji Nieprawidłowy lub wygasły token Żądanie nowego tokenu od klienta
403 Zabronione Brak wymaganych zakresów Aktualizowanie konfiguracji zakresu lub żądania tokenu
400 Nieprawidłowe żądanie Źle sformułowany nagłówek autoryzacji Sprawdź format nagłówka: ******

Struktura odpowiedzi

Punkt /Validate końcowy zwraca:

{
  "protocol": "Bearer",
  "token": "******",
  "claims": {
    "aud": "api://your-api-id",
    "iss": "https://sts.windows.net/tenant-id/",
    "iat": 1234567890,
    "nbf": 1234567890,
    "exp": 1234571490,
    "oid": "user-object-id",
    "sub": "subject",
    "tid": "tenant-id",
    "upn": "user@contoso.com",
    "scp": "User.Read Mail.Read",
    "roles": ["Admin"]
  }
}

Najlepsze praktyki

  1. Sprawdzanie poprawności na wczesnym etapie: sprawdzanie poprawności tokenów w bramie interfejsu API lub w punkcie wejścia
  2. Sprawdź zakresy: zawsze sprawdź, czy token ma wymagane zakresy dla operacji
  3. Niepowodzenia logowania: niepowodzenia weryfikacji dziennika na potrzeby monitorowania zabezpieczeń
  4. Obsługa błędów: podaj jasne komunikaty o błędach na potrzeby debugowania
  5. Użyj oprogramowania pośredniczącego: zaimplementuj walidację jako oprogramowanie pośredniczące w celu zapewnienia spójności
  6. Bezpieczny zestaw SDK: upewnij się, że zestaw SDK jest dostępny tylko z poziomu aplikacji

Dalsze kroki

Po zweryfikowaniu tokenów może być konieczne: