From 473ac6e88983dd41c7a36c389b5724d123d27faa Mon Sep 17 00:00:00 2001 From: warrenchen Date: Fri, 17 Jul 2026 23:09:04 +0900 Subject: [PATCH] Remove API registration endpoint and trust forwarded host --- docs/CONFIGURATION.md | 4 ++- docs/INSTALL.md | 3 ++ docs/openapi.yaml | 25 ----------------- .../Contracts/AuthRequests.cs | 2 -- .../Controllers/AuthController.cs | 28 ------------------- .../MemberCenter.Infrastructure.csproj | 1 + .../SecurityConfigurationTests.cs | 28 +++++++++++++++++-- 7 files changed, 33 insertions(+), 58 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index fb66a66..001542a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -38,7 +38,7 @@ API、Web 與 Installer 共用上述規則。 | `RateLimits:Web:AuthRegister` | 5 | 900 | Web register | | `RateLimits:Web:AuthRecovery` | 5 | 900 | Web forgot/resend | | `RateLimits:Web:AuthTokenConsumption` | 10 | 600 | Web reset/verify | -| `RateLimits:Api:AuthRegister` | 5 | 900 | API register | +| `RateLimits:Api:AuthRegister` | 5 | 900 | 保留設定;API register endpoint 目前未開放。 | | `RateLimits:Api:AuthRecovery` | 5 | 900 | API forgot/resend | | `RateLimits:Api:AuthTokenConsumption` | 10 | 600 | API reset/verify | | `RateLimits:Api:NewsletterSubscribe` | 20 | 600 | Newsletter subscribe | @@ -101,6 +101,8 @@ OAuth usage/scope mapping 的正式來源為 DB registry;audience key 只作 s `ReverseProxy:TrustForwardedHeaders=true` 會接受 `X-Forwarded-For`、`X-Forwarded-Proto`、`X-Forwarded-Host`。此模式適合 AWS ALB / managed reverse proxy private IP 會變動,但 app security group 已只允許該 proxy 連入的環境。 +接受 `X-Forwarded-Host` 是刻意支援 AWS ALB 後方的登入、OAuth callback 與 public URL 產生。Production 必須同時由 ALB / router 覆寫外部 forwarded headers、以 Security Group 阻止繞過 proxy,並將 DB `public_base_url` 設為 canonical HTTPS URL。部署驗證需包含偽造 `Host` / `X-Forwarded-Host` 的 redirect 與 email link 測試。 + 未啟用 `TrustForwardedHeaders` 時,必須設定 `KnownProxies` 或 `KnownNetworks` 才會接受 forwarded headers;allowlist 都為空時完全忽略 forwarded headers。 ## 外部整合與測試旗標 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 7ff1df4..77c1852 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -95,6 +95,9 @@ Reverse proxy 信任設定: - `ReverseProxy__TrustForwardedHeaders=true` - `ReverseProxy__ForwardLimit=1` - `ReverseProxy__TrustForwardedHeaders=true` 會接受 `X-Forwarded-For`、`X-Forwarded-Proto`、`X-Forwarded-Host`;只有在 app inbound 已由 Security Group / 私有網路限制為可信 proxy 時才可使用。 +- 接受 `X-Forwarded-Host` 是為了讓 AWS ALB 後方的登入、OAuth callback 與外部 URL 產生使用原始 public host;ALB / router 必須覆寫外部傳入的 forwarded headers,不可直接沿用任意 client 值。 +- Production 的 `/admin/security` `Public base URL` 必須設定為 canonical HTTPS URL,讓驗證信與密碼重設信不依賴 request host。 +- 部署後必須以偽造 `Host` / `X-Forwarded-Host` 實測登入 redirect、OAuth callback 與 email link,確認不會導向非預期網域。 - 若不使用 `TrustForwardedHeaders`,未設定 `ReverseProxy__KnownProxies` / `ReverseProxy__KnownNetworks` 時,API 與 Web 完全忽略 forwarded headers。 - `ReverseProxy__KnownProxies` 使用逗號分隔 IP,例如 `10.0.0.10,10.0.0.11`。 - `ReverseProxy__KnownNetworks` 使用逗號分隔 CIDR,例如 `10.0.0.0/24,fd00::/64`。 diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 4cfa9fa..55e23a6 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -84,24 +84,6 @@ paths: '200': description: JSON Web Key Set - /auth/register: - post: - summary: Register user - security: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RegisterRequest' - responses: - '200': - description: Registered - content: - application/json: - schema: - $ref: '#/components/schemas/UserProfile' - /auth/logout: post: summary: Logout current authenticated session @@ -901,13 +883,6 @@ components: token_type: { type: string, example: Bearer } expires_in: { type: integer } - RegisterRequest: - type: object - required: [email, password] - properties: - email: { type: string, format: email } - password: { type: string } - AuthorizationCodeTokenRequest: type: object required: [grant_type, code, redirect_uri, code_verifier] diff --git a/src/MemberCenter.Api/Contracts/AuthRequests.cs b/src/MemberCenter.Api/Contracts/AuthRequests.cs index 5f63e65..e6c3883 100644 --- a/src/MemberCenter.Api/Contracts/AuthRequests.cs +++ b/src/MemberCenter.Api/Contracts/AuthRequests.cs @@ -1,7 +1,5 @@ namespace MemberCenter.Api.Contracts; -public sealed record RegisterRequest(string Email, string Password); - public sealed record RefreshRequest(string RefreshToken); public sealed record ForgotPasswordRequest(string Email); diff --git a/src/MemberCenter.Api/Controllers/AuthController.cs b/src/MemberCenter.Api/Controllers/AuthController.cs index 7bbedab..420317e 100644 --- a/src/MemberCenter.Api/Controllers/AuthController.cs +++ b/src/MemberCenter.Api/Controllers/AuthController.cs @@ -13,51 +13,23 @@ namespace MemberCenter.Api.Controllers; [Route("auth")] public class AuthController : ControllerBase { - private readonly IAccountProvisioningService _accountProvisioningService; private readonly IAccountEmailService _accountEmailService; private readonly IAuditLogWriter _auditLogWriter; private readonly UserManager _userManager; private readonly SignInManager _signInManager; public AuthController( - IAccountProvisioningService accountProvisioningService, IAccountEmailService accountEmailService, IAuditLogWriter auditLogWriter, UserManager userManager, SignInManager signInManager) { - _accountProvisioningService = accountProvisioningService; _accountEmailService = accountEmailService; _auditLogWriter = auditLogWriter; _userManager = userManager; _signInManager = signInManager; } - [HttpPost("register")] - [EnableRateLimiting(RateLimitPolicyNames.PublicAuthRegister)] - public async Task Register([FromBody] RegisterRequest request) - { - var result = await _accountProvisioningService.RegisterLocalAsync(request.Email, request.Password); - if (!result.Succeeded) - { - return BadRequest(result.Errors); - } - - var user = await _userManager.FindByEmailAsync(request.Email); - if (user is not null) - { - await _accountEmailService.SendVerificationEmailAsync(user.Id, GetBaseUrl()); - } - - return Ok(new - { - id = result.UserId, - email = result.Email, - email_verified = result.EmailConfirmed, - linked_subscriptions = result.LinkedSubscriptionsCount - }); - } - [HttpPost("password/forgot")] [EnableRateLimiting(RateLimitPolicyNames.PublicAuthRecovery)] public async Task ForgotPassword([FromBody] ForgotPasswordRequest request) diff --git a/src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj b/src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj index 56b5de7..2f65a74 100644 --- a/src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj +++ b/src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj @@ -20,6 +20,7 @@ + diff --git a/tests/MemberCenter.Security.Tests/SecurityConfigurationTests.cs b/tests/MemberCenter.Security.Tests/SecurityConfigurationTests.cs index 27ed15c..a163214 100644 --- a/tests/MemberCenter.Security.Tests/SecurityConfigurationTests.cs +++ b/tests/MemberCenter.Security.Tests/SecurityConfigurationTests.cs @@ -42,7 +42,7 @@ public sealed class SecurityConfigurationTests } [Fact] - public void TrustedProxyAllowlistEnablesExpectedHeaders() + public void TrustedProxyAllowlistIncludesForwardedHost() { var options = new ForwardedHeadersOptions(); TrustedForwardedHeaders.Configure(options, Configuration(new() @@ -51,11 +51,35 @@ public sealed class SecurityConfigurationTests ["ReverseProxy:KnownNetworks"] = "10.1.0.0/16" })); - Assert.Equal(ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, options.ForwardedHeaders); + Assert.Equal( + ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost, + options.ForwardedHeaders); Assert.Single(options.KnownProxies); Assert.Single(options.KnownNetworks); } + [Fact] + public void ManagedProxyModeIncludesForwardedHostForExternalUrlGeneration() + { + var options = new ForwardedHeadersOptions(); + TrustedForwardedHeaders.Configure(options, Configuration(new() + { + ["ReverseProxy:TrustForwardedHeaders"] = "true", + ["ReverseProxy:ForwardLimit"] = "1" + })); + + Assert.Equal( + ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost, + options.ForwardedHeaders); + Assert.Equal(1, options.ForwardLimit); + Assert.Empty(options.KnownProxies); + Assert.Empty(options.KnownNetworks); + } + [Theory] [InlineData("0.0.0.0/0")] [InlineData("::/0")]