들어오는 전달자 토큰을 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"]
}
}
모범 사례
- 조기 유효성 검사: API 게이트웨이 또는 진입점에서 토큰 유효성 검사
- 범위 확인: 항상 토큰에 작업에 필요한 범위가 있는지 확인
- 로그 오류: 보안 모니터링에 대한 로그 유효성 검사 실패
- 오류 처리: 디버깅을 위한 명확한 오류 메시지 제공
- 미들웨어 사용: 일관성을 위한 미들웨어로 유효성 검사 구현
- 보안 SDK: 애플리케이션에서만 SDK에 액세스할 수 있는지 확인
다음 단계
토큰의 유효성을 검사한 후 다음을 수행해야 할 수 있습니다.