다음을 통해 공유


시나리오: 권한 부여 헤더 유효성 검사

들어오는 전달자 토큰을 AgentID의 /Validate 엔드포인트용 Microsoft Entra SDK에 전달하여 유효성을 검사한 다음 반환된 클레임을 추출하여 권한 부여를 결정합니다. 이 가이드에서는 토큰 유효성 검사 미들웨어를 구현하고 범위 또는 역할에 따라 권한 부여를 결정하는 방법을 보여 줍니다.

필수 조건

  • 활성 구독이 있는 Azure 계정. 무료로 계정을 만듭니다.
  • 애플리케이션에서 네트워크 액세스를 사용하여 배포되고 실행되는 AgentID용 Microsoft Entra SDK입니다. 설치 지침은 설치 가이드 를 참조하세요.
  • Microsoft Entra ID에 등록된 애플리케이션 - 이 조직 디렉터리의 계정에 대해서만 구성된 Microsoft Entra 관리 센터에 새 앱을 등록합니다. 애플리케이션 등록에서 자세한 내용을 참조하세요. 애플리케이션 개요 페이지에서 다음 값을 기록합니다.
    • 애플리케이션(클라이언트) ID
    • 디렉터리(테넌트) ID
    • API 노출 섹션에서 앱 ID URI 구성(토큰 유효성 검사 대상 그룹으로 사용)
  • 인증된 클라이언트의 전달자 토큰 - 애플리케이션은 OAuth 2.0 흐름을 통해 클라이언트 애플리케이션에서 토큰을 받아야 합니다.
  • Microsoft Entra ID의 적절한 권한 - 계정에 애플리케이션을 등록하고 인증 설정을 구성할 수 있는 권한이 있어야 합니다.

구성 / 설정

API에 대한 토큰의 유효성을 검사하려면 Microsoft Entra ID 테넌트 정보를 사용하여 AgentID용 Microsoft Entra SDK를 구성합니다.

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

다음 구현에서는 TypeScript 또는 JavaScript를 사용하여 AgentID용 Microsoft Entra SDK와 통합되는 토큰 유효성 검사 미들웨어를 만드는 방법을 보여 줍니다. 이 미들웨어는 들어오는 모든 요청에서 유효한 전달자 토큰을 확인하고 경로 처리기에서 사용할 클레임을 추출합니다.

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;
}

다음 코드 조각은 Express.js 미들웨어에서 함수를 사용하여 validateToken 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 코드 조각은 Flask 데코레이터를 사용하여 경로 처리기를 토큰 유효성 검사로 래핑합니다. 이 데코레이터는 승인 헤더에서 전달자 토큰을 추출하고, Microsoft Entra SDK를 사용하여 AgentID에 대한 유효성을 검사하며, 경로 내에서 클레임을 사용할 수 있도록 합니다.

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

다음 Go 구현에서는 표준 HTTP 처리기 패턴을 사용하여 토큰 유효성 검사를 보여 줍니다. 이 미들웨어 접근 방식은 권한 부여 헤더에서 전달자 토큰을 추출하고, AgentID용 Microsoft Entra SDK로 유효성을 검사하고, 다운스트림 처리기에서 사용할 요청 헤더에 사용자 정보를 저장합니다.

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#

다음 C# 구현에서는 ASP.NET Core 미들웨어를 사용한 토큰 유효성 검사를 보여 줍니다. 이 방법은 종속성 주입을 사용하여 토큰 유효성 검사 서비스에 액세스하고, 권한 부여 헤더에서 전달자 토큰을 추출하고, AgentID용 Microsoft Entra SDK로 유효성을 검사하고, 컨트롤러에서 사용할 사용자 클레임을 HttpContext에 저장합니다.

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 }
        });
    }
}

특정 클레임 추출

토큰의 유효성을 검사한 후 클레임을 추출하여 애플리케이션에서 권한 부여 결정을 내릴 수 있습니다. 엔드포인트 /Validate는 다음과 같은 정보를 포함한 클레임 객체를 반환합니다.

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

일반적인 클레임은 다음과 같습니다.

  • oid: Microsoft Entra ID 테넌트에서 개체 식별자(고유 사용자 ID)
  • upn: 사용자 계정 이름(일반적으로 전자 메일 형식)
  • tid: 사용자가 속한 테넌트 ID
  • scp: 사용자가 애플리케이션에 부여한 위임된 범위
  • roles: 사용자에게 할당된 애플리케이션 역할

다음 예제에서는 유효성 검사 응답에서 특정 클레임을 추출하는 방법을 보여 줍니다.

사용자 ID:

// 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

범위 및 역할:

// 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
}

권한 부여 패턴

토큰의 유효성을 검사한 후 위임된 범위(사용자가 부여한 권한) 또는 애플리케이션 역할(테넌트 관리자가 할당)에 따라 권한 부여를 적용할 수 있습니다. 권한 부여 모델과 일치하는 패턴을 선택합니다.

범위 기반 권한 부여

액세스 권한을 부여하기 전에 사용자 토큰에 필요한 범위가 포함되어 있는지 확인합니다.

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' });
});

역할 기반 권한 부여

사용자에게 필요한 애플리케이션 역할이 있는지 확인합니다.

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' });
});

오류 처리

토큰 유효성 검사는 여러 가지 이유로 실패할 수 있습니다. 토큰이 만료되었거나, 유효하지 않거나, 필요한 범위가 누락될 수 있습니다. 적절하게 응답할 수 있도록 다양한 오류 시나리오를 구분하는 오류 처리를 구현합니다.

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;
  }
}

일반적인 유효성 검사 오류

오류 원인 해결 방법
401 권한 없음 유효하지 않거나 만료된 토큰 클라이언트에서 새 토큰 요청
403 금지됨 필수 범위 누락 범위 구성 또는 토큰 요청 업데이트
400 잘못된 요청 잘못된 형식의 권한 부여 헤더 헤더 형식 확인: ******

응답 구조

/Validate 엔드포인트는 다음을 반환합니다.

{
  "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"]
  }
}

모범 사례

  1. 조기 유효성 검사: API 게이트웨이 또는 진입점에서 토큰 유효성 검사
  2. 범위 확인: 항상 토큰에 작업에 필요한 범위가 있는지 확인
  3. 로그 오류: 보안 모니터링에 대한 로그 유효성 검사 실패
  4. 오류 처리: 디버깅을 위한 명확한 오류 메시지 제공
  5. 미들웨어 사용: 일관성을 위한 미들웨어로 유효성 검사 구현
  6. 보안 SDK: 애플리케이션에서만 SDK에 액세스할 수 있는지 확인

다음 단계

토큰의 유효성을 검사한 후 다음을 수행해야 할 수 있습니다.