From bb219fcbb3088464ee36c36fb3f5956da8e8e6d8 Mon Sep 17 00:00:00 2001 From: Warren Chen Date: Tue, 7 Jul 2026 13:27:18 +0900 Subject: [PATCH] Support metadata address and scp scope validation --- README.md | 15 ++++++++++++--- docs/API.md | 5 +++-- src/FileAccessAgent.Api/Program.cs | 7 +++++++ src/FileAccessAgent.Api/appsettings.json | 1 + ...MemberCenterDelegatedDownloadTokenValidator.cs | 2 +- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5f679ca..830c45c 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,11 @@ Member Center 是 OAuth2/OIDC issuer 與 JWKS 提供者。File Access Agent 不 - `files:metadata.read` - `files:delete` -最新版 `member_center` 文件已明確把 File Access 納入同一套租戶授權模型:`file_api` client 必須綁定 `tenant_id`,upload token 與 delegated download token 也都必須帶 `tenant_id`。 +最新版 `member_center` 文件已明確把 File Access 納入同一套租戶授權模型:`file_api` client 必須綁定 `tenant_id`,upload token 與 delegated download token 也都必須帶 `tenant_id`。Client Credentials 可核發的 scope 由 Member Center 的 OAuth application `usage` 對應 mapping 決定,不應依賴呼叫端用 `scope` 參數自行擴權。 File Access Agent 應驗證: -- upload 的 S2S JWT:透過 Member Center JWKS 驗 `iss`、`aud=file_access_api`、`exp`、`scope`、`tenant_id` +- upload 的 S2S JWT:透過 Member Center JWKS 驗 `iss`、`aud=file_access_api`、`exp`、`scope`/`scp`、`tenant_id` - download 的 delegated token:以 Member Center validation endpoint 線上驗證 - token 中的 `tenant_id` 是否符合實際 request - token 中的 `object_key` 或 `file_id` 是否符合實際 request @@ -175,6 +175,15 @@ docker run --rm -p 8080:8080 \ file-access-agent-api ``` +若 production 的 token `iss` 是公開 HTTPS issuer,但 Agent 需要透過內網網址讀取 OIDC metadata/JWKS,可保留 `Auth__Issuer` 為公開 issuer,並另外設定 `Auth__MetadataAddress`,例如: + +```bash +-e Auth__Issuer=https://member.example.com/ \ +-e Auth__MetadataAddress=http://member-center-api/.well-known/openid-configuration +``` + +`MemberCenter__BaseUrl` 則是 Agent 呼叫 `/oauth/token` 與 delegated download token validation endpoint 的 server-to-server base URL,可依部署網路使用內網位址。 + 若使用 local storage,建議掛載 volume,避免 container 移除後檔案遺失: ```bash @@ -261,4 +270,4 @@ docker run --rm -p 8080:8080 \ - 已確認使用 C# / ASP.NET Core。 - `tenant_id` 與 delegated download token 分工已依最新版 `member_center` 文件校正。 - API 已提供 Dockerfile,可用 .NET 8 多階段建置產生 container image。 -- 目前目錄中的 .NET skeleton 可作為正式起點,但仍需依最新流程補上 Member Center validation 串接。 +- API 已實作 Member Center JWT 驗證、delegated download token validation 串接,以及 local / MinIO storage adapter。 diff --git a/docs/API.md b/docs/API.md index 54821ae..378b2b8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -5,7 +5,7 @@ 目前狀態: - File Access Agent API:本文件定義為本專案目標契約。 -- Member Center File Access API:依 `../member_center/docs/openapi.yaml` 與相關文件整理,作為本專案整合依據。 +- Member Center File Access API:依 `../member_center/docs/openapi.yaml` 與相關文件整理,作為本專案整合依據。Client Credentials 可核發的 scope 由 Member Center 的 OAuth application `usage` mapping 決定,不依賴呼叫端自行擴權。 ## 0. 部署與資料假設 @@ -51,6 +51,7 @@ - 必要 scope:`files:upload.write` - 必要 claim:`tenant_id` - 驗證方式:File Access Agent 以 Member Center JWKS 驗簽 JWT +- 若 token issuer 是公開 HTTPS 位址,但 JWKS 需走內網位址,可設定 `Auth:MetadataAddress` 覆寫 metadata discovery URL,`iss` 仍以 `Auth:Issuer` 驗證。 ### 2.2 Download @@ -116,7 +117,7 @@ Validation: - JWT signature / `iss` / `aud=file_access_api` / `exp` - token `tenant_id` -- `scope=files:upload.write` +- `scope` 或 `scp` 包含 `files:upload.write` - request `objectKey` 是否符合允許的 tenant 邊界策略 Object key 命名建議(介接規範): diff --git a/src/FileAccessAgent.Api/Program.cs b/src/FileAccessAgent.Api/Program.cs index 3cbea63..0fe6e9b 100644 --- a/src/FileAccessAgent.Api/Program.cs +++ b/src/FileAccessAgent.Api/Program.cs @@ -43,11 +43,16 @@ builder.Services .AddJwtBearer(options => { options.Authority = authOptions.Issuer; + if (!string.IsNullOrWhiteSpace(authOptions.MetadataAddress)) + { + options.MetadataAddress = authOptions.MetadataAddress; + } options.Audience = authOptions.Audience; options.RequireHttpsMetadata = authOptions.RequireHttpsMetadata; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, + ValidIssuer = authOptions.Issuer, ValidateAudience = true, ValidAudience = authOptions.Audience, ValidateLifetime = true @@ -317,6 +322,7 @@ static IResult? EnsureJwtScopeAndTenant(HttpContext context, string requiredScop static bool HasScope(ClaimsPrincipal user, string expectedScope) { var scopes = user.FindAll("scope") + .Concat(user.FindAll("scp")) .SelectMany(claim => claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); return scopes.Contains(expectedScope, StringComparer.Ordinal); } @@ -379,6 +385,7 @@ public sealed class AuthOptions public const string SectionName = "Auth"; public string Issuer { get; set; } = string.Empty; + public string? MetadataAddress { get; set; } public string Audience { get; set; } = "file_access_api"; public bool RequireHttpsMetadata { get; set; } = true; } diff --git a/src/FileAccessAgent.Api/appsettings.json b/src/FileAccessAgent.Api/appsettings.json index 10d8998..3a50357 100644 --- a/src/FileAccessAgent.Api/appsettings.json +++ b/src/FileAccessAgent.Api/appsettings.json @@ -22,6 +22,7 @@ }, "Auth": { "Issuer": "http://localhost:7850/", + "MetadataAddress": "", "Audience": "file_access_api", "RequireHttpsMetadata": false }, diff --git a/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs b/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs index f239f7b..6b70343 100644 --- a/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs +++ b/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs @@ -119,7 +119,7 @@ public sealed class MemberCenterDelegatedDownloadTokenValidator : IDelegatedDown } _cachedAccessToken = token.AccessToken; - _cachedAccessTokenExpiresAt = DateTimeOffset.UtcNow.AddSeconds(Math.Max(30, token.ExpiresIn)); + _cachedAccessTokenExpiresAt = DateTimeOffset.UtcNow.AddSeconds(Math.Max(0, token.ExpiresIn)); return _cachedAccessToken; } finally