From 09589ef631700301598cb97c3c9f24ce7ad312d0 Mon Sep 17 00:00:00 2001 From: warrenchen Date: Thu, 23 Apr 2026 13:56:33 +0900 Subject: [PATCH] Completed API and redirect login test feat: Update OAuth client handling and permissions, enhance authentication claims, and adjust environment settings --- .env.example | 2 + .../AdminOAuthClientsController.cs | 5 ++ .../Controllers/OAuthController.cs | 1 + .../Controllers/TokenController.cs | 1 + src/MemberCenter.Api/Program.cs | 6 ++ .../Properties/launchSettings.json | 2 +- .../Services/AuthResourceRegistryService.cs | 7 +- src/MemberCenter.Installer/Program.cs | 89 +++++++++++++++++++ src/MemberCenter.TestSite/appsettings.json | 6 +- .../Controllers/OAuthClientsController.cs | 5 ++ 10 files changed, 119 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 63cea24..2122eb6 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,8 @@ ASPNETCORE_ENVIRONMENT=Development ConnectionStrings__Default=Host=localhost;Database=member_center;Username=postgres;Password=postgres Auth__Issuer=http://localhost:7850/ +Auth__WebLoginUrl=http://localhost:5080/account/login +Auth__AllowedLoginReturnUrlPrefixes=http://localhost:7850/ Auth__MemberCenterAudience=member_center_api Auth__SendEngineAudience=send_engine_api SendEngine__BaseUrl=http://localhost:6060 diff --git a/src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs b/src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs index d69e730..dd66aac 100644 --- a/src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs +++ b/src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs @@ -162,6 +162,11 @@ public class AdminOAuthClientsController : ControllerBase descriptor.DisplayName = request.Name; descriptor.ClientType = request.ClientType; + if (string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) + { + descriptor.ClientSecret = null; + } + await ApplyPermissionsAsync(descriptor, request.Usage); descriptor.RedirectUris.Clear(); foreach (var uri in redirectUris) diff --git a/src/MemberCenter.Api/Controllers/OAuthController.cs b/src/MemberCenter.Api/Controllers/OAuthController.cs index 7a85c27..1815517 100644 --- a/src/MemberCenter.Api/Controllers/OAuthController.cs +++ b/src/MemberCenter.Api/Controllers/OAuthController.cs @@ -56,6 +56,7 @@ public class OAuthController : ControllerBase } var principal = await _signInManager.CreateUserPrincipalAsync(user); + principal.SetClaim(OpenIddictConstants.Claims.Subject, user.Id.ToString()); if (!string.IsNullOrWhiteSpace(user.SecurityStamp)) { principal.SetClaim(SecurityStampClaimType, user.SecurityStamp); diff --git a/src/MemberCenter.Api/Controllers/TokenController.cs b/src/MemberCenter.Api/Controllers/TokenController.cs index 213167d..a8cab7f 100644 --- a/src/MemberCenter.Api/Controllers/TokenController.cs +++ b/src/MemberCenter.Api/Controllers/TokenController.cs @@ -58,6 +58,7 @@ public class TokenController : ControllerBase } var principal = await _signInManager.CreateUserPrincipalAsync(user); + principal.SetClaim(OpenIddictConstants.Claims.Subject, user.Id.ToString()); if (!string.IsNullOrWhiteSpace(user.SecurityStamp)) { principal.SetClaim(SecurityStampClaimType, user.SecurityStamp); diff --git a/src/MemberCenter.Api/Program.cs b/src/MemberCenter.Api/Program.cs index cbf9971..e7b060a 100644 --- a/src/MemberCenter.Api/Program.cs +++ b/src/MemberCenter.Api/Program.cs @@ -231,6 +231,12 @@ app.Use(async (context, next) => { if (context.User.Identity?.IsAuthenticated == true) { + if (context.User.HasClaim(claim => claim.Type == "client_usage")) + { + await next(); + return; + } + var subject = context.User.FindFirstValue(OpenIddictConstants.Claims.Subject) ?? context.User.FindFirstValue(ClaimTypes.NameIdentifier); diff --git a/src/MemberCenter.Api/Properties/launchSettings.json b/src/MemberCenter.Api/Properties/launchSettings.json index f032fb1..607898b 100644 --- a/src/MemberCenter.Api/Properties/launchSettings.json +++ b/src/MemberCenter.Api/Properties/launchSettings.json @@ -13,7 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5050", + "applicationUrl": "http://localhost:7850", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/MemberCenter.Infrastructure/Services/AuthResourceRegistryService.cs b/src/MemberCenter.Infrastructure/Services/AuthResourceRegistryService.cs index 935d0c2..be95a5c 100644 --- a/src/MemberCenter.Infrastructure/Services/AuthResourceRegistryService.cs +++ b/src/MemberCenter.Infrastructure/Services/AuthResourceRegistryService.cs @@ -79,7 +79,12 @@ public sealed class AuthResourceRegistryService : IAuthResourceRegistryService OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile, - "profile:basic.read" + "profile:basic.read", + "profile:basic.write", + "profile:addresses.read", + "profile:addresses.write", + "profile:subscriptions.read", + "profile:subscriptions.write" ], cancellationToken); await EnsureUsagePermissionsAsync("webhook_outbound", [ diff --git a/src/MemberCenter.Installer/Program.cs b/src/MemberCenter.Installer/Program.cs index e1192f3..45c71b1 100644 --- a/src/MemberCenter.Installer/Program.cs +++ b/src/MemberCenter.Installer/Program.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Npgsql; +using OpenIddict.Abstractions; using System.Text.Json; using System.Text.Json.Nodes; using System.CommandLine; @@ -283,10 +284,60 @@ migrateCommand.SetHandler(async (string? connectionString, string? appsettings, Console.WriteLine("Migrations completed."); }, connectionStringOption, appsettingsOption, targetMigrationOption); +var syncOAuthClientsCommand = new Command("sync-oauth-clients", "Refresh OAuth client permissions from usage defaults"); +syncOAuthClientsCommand.AddOption(connectionStringOption); +syncOAuthClientsCommand.AddOption(appsettingsOption); +syncOAuthClientsCommand.SetHandler(async (string? connectionString, string? appsettings) => +{ + var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt: true); + if (string.IsNullOrWhiteSpace(resolvedConnection)) + { + Console.Error.WriteLine("Connection string is required."); + return; + } + + var services = BuildServices(resolvedConnection); + await using var scope = services.CreateAsyncScope(); + var registry = scope.ServiceProvider.GetRequiredService(); + await registry.EnsureDefaultsAsync(); + + var applicationManager = scope.ServiceProvider.GetRequiredService(); + var applications = new List(); + await foreach (var application in applicationManager.ListAsync()) + { + applications.Add(application); + } + + var updated = 0; + foreach (var application in applications) + { + var properties = await applicationManager.GetPropertiesAsync(application); + if (!properties.TryGetValue("usage", out var usageElement)) + { + continue; + } + + var usage = usageElement.GetString(); + if (string.IsNullOrWhiteSpace(usage)) + { + continue; + } + + var descriptor = new OpenIddictApplicationDescriptor(); + await applicationManager.PopulateAsync(descriptor, application); + await ApplyOAuthClientPermissionsAsync(descriptor, registry, usage); + await applicationManager.UpdateAsync(application, descriptor); + updated++; + } + + Console.WriteLine($"OAuth clients synchronized: {updated}."); +}, connectionStringOption, appsettingsOption); + root.AddCommand(initCommand); root.AddCommand(addAdminCommand); root.AddCommand(resetCommand); root.AddCommand(migrateCommand); +root.AddCommand(syncOAuthClientsCommand); return await root.InvokeAsync(args); @@ -303,6 +354,13 @@ static IServiceProvider BuildServices(string connectionString) options.UseOpenIddict(); }); + services.AddOpenIddict() + .AddCore(options => + { + options.UseEntityFrameworkCore() + .UseDbContext(); + }); + services.AddIdentity(options => { options.User.RequireUniqueEmail = true; @@ -320,6 +378,37 @@ static IServiceProvider BuildServices(string connectionString) return services.BuildServiceProvider(); } +static async Task ApplyOAuthClientPermissionsAsync( + OpenIddictApplicationDescriptor descriptor, + IAuthResourceRegistryService registry, + string usage) +{ + descriptor.Permissions.Clear(); + descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token); + + if (UsesAuthorizationCodeFlow(usage)) + { + descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization); + descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode); + descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken); + descriptor.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code); + } + else + { + descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials); + } + + var scopes = await registry.GetAllowedScopesForUsageAsync(usage); + foreach (var scope in scopes) + { + descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope); + } +} + +static bool UsesAuthorizationCodeFlow(string usage) => + string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase) + || string.Equals(usage, "webhook_outbound", StringComparison.OrdinalIgnoreCase); + static string? ResolveConnectionString(string? connectionString, string? appsettingsPath, bool noPrompt) { var targetPath = ResolveAppsettingsPath(appsettingsPath); diff --git a/src/MemberCenter.TestSite/appsettings.json b/src/MemberCenter.TestSite/appsettings.json index 28b493e..6fcf7c3 100644 --- a/src/MemberCenter.TestSite/appsettings.json +++ b/src/MemberCenter.TestSite/appsettings.json @@ -1,11 +1,11 @@ { "MemberCenter": { "ApiBaseUrl": "http://localhost:7850", - "WebLoginClientId": "", + "WebLoginClientId": "f48329ef38c54a62b627585a75c9b5d5", "WebLoginRedirectPath": "/auth/callback", "WebLoginScopes": "openid email profile profile:basic.read profile:basic.write profile:addresses.read profile:addresses.write profile:subscriptions.read profile:subscriptions.write", - "ServiceClientId": "", - "ServiceClientSecret": "", + "ServiceClientId": "e9fe7ae413c54ae49432eca6474648d3", + "ServiceClientSecret": "To/WaVObQgxCaZwzGexfp/pvUwI2G5o1r55v4sRukYw=", "ServiceScopes": "profile:basic.read profile:addresses.read" }, "Logging": { diff --git a/src/MemberCenter.Web/Areas/Admin/Controllers/OAuthClientsController.cs b/src/MemberCenter.Web/Areas/Admin/Controllers/OAuthClientsController.cs index 42f4927..5f07b71 100644 --- a/src/MemberCenter.Web/Areas/Admin/Controllers/OAuthClientsController.cs +++ b/src/MemberCenter.Web/Areas/Admin/Controllers/OAuthClientsController.cs @@ -204,6 +204,11 @@ public class OAuthClientsController : Controller descriptor.DisplayName = model.Name; descriptor.ClientType = model.ClientType; + if (string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) + { + descriptor.ClientSecret = null; + } + await ApplyPermissionsAsync(descriptor, model.Usage); descriptor.RedirectUris.Clear(); foreach (var uri in redirectUris)