using Fengling.AuthService.Models; using Fengling.Console.Repositories; using OpenIddict.Abstractions; using OpenIddict.Server; using System.Security.Cryptography; namespace Fengling.Console.Services; public interface IOAuthClientService { Task<(IEnumerable Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null); Task GetClientAsync(long id); Task CreateClientAsync(OAuthApplication client); Task GenerateSecretAsync(long id); Task ToggleStatusAsync(long id); Task UpdateClientAsync(long id, OAuthApplication updatedClient); Task DeleteClientAsync(long id); object GetClientOptions(); } public class OAuthClientService : IOAuthClientService { private readonly IOAuthClientRepository _repository; private readonly IOpenIddictApplicationManager _applicationManager; private readonly ILogger _logger; public OAuthClientService( IOAuthClientRepository repository, IOpenIddictApplicationManager applicationManager, ILogger logger) { _repository = repository; _applicationManager = applicationManager; _logger = logger; } public async Task<(IEnumerable Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null) { var items = await _repository.GetPagedAsync(page, pageSize, displayName, clientId, status); var totalCount = await _repository.CountAsync(displayName, clientId, status); return (items, totalCount); } public async Task GetClientAsync(long id) { return await _repository.GetByIdAsync(id); } public async Task CreateClientAsync(OAuthApplication client) { var existing = await _repository.GetByClientIdAsync(client.ClientId); if (existing != null) { throw new InvalidOperationException($"Client ID '{client.ClientId}' 已存在"); } var descriptor = new OpenIddictApplicationDescriptor { ClientId = client.ClientId, ClientSecret = client.ClientSecret, DisplayName = client.DisplayName }; foreach (var uri in client.RedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.RedirectUris.Add(new Uri(uri)); } foreach (var uri in client.PostLogoutRedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); } foreach (var grantType in client.GrantTypes) { descriptor.Permissions.Add(grantType); } foreach (var scope in client.Scopes) { descriptor.Permissions.Add(scope); } await _applicationManager.CreateAsync(descriptor); await _repository.AddAsync(client); _logger.LogInformation("Created OAuth client {ClientId}", client.ClientId); return client; } public async Task GenerateSecretAsync(long id) { var client = await _repository.GetByIdAsync(id); if (client == null) { throw new KeyNotFoundException($"Client with ID {id} not found"); } var application = await _applicationManager.FindByClientIdAsync(client.ClientId); if (application == null) { throw new KeyNotFoundException($"OpenIddict application for client {client.ClientId} not found"); } var newSecret = GenerateSecureSecret(); var descriptor = new OpenIddictApplicationDescriptor { ClientId = client.ClientId, ClientSecret = newSecret, DisplayName = client.DisplayName }; foreach (var uri in client.RedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.RedirectUris.Add(new Uri(uri)); } foreach (var uri in client.PostLogoutRedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); } foreach (var grantType in client.GrantTypes) { descriptor.Permissions.Add(grantType); } foreach (var scope in client.Scopes) { descriptor.Permissions.Add(scope); } await _applicationManager.UpdateAsync(application, descriptor); client.ClientSecret = newSecret; client.UpdatedAt = DateTime.UtcNow; await _repository.UpdateAsync(client); _logger.LogInformation("Generated new secret for OAuth client {ClientId}", client.ClientId); return client; } public async Task ToggleStatusAsync(long id) { var client = await _repository.GetByIdAsync(id); if (client == null) { throw new KeyNotFoundException($"Client with ID {id} not found"); } var oldStatus = client.Status; client.Status = client.Status == "active" ? "inactive" : "active"; client.UpdatedAt = DateTime.UtcNow; await _repository.UpdateAsync(client); _logger.LogInformation("Toggled OAuth client {ClientId} status from {OldStatus} to {NewStatus}", client.ClientId, oldStatus, client.Status); return client; } public async Task UpdateClientAsync(long id, OAuthApplication updatedClient) { var client = await _repository.GetByIdAsync(id); if (client == null) { throw new KeyNotFoundException($"Client with ID {id} not found"); } var application = await _applicationManager.FindByClientIdAsync(client.ClientId); if (application == null) { throw new KeyNotFoundException($"OpenIddict application for client {client.ClientId} not found"); } var descriptor = new OpenIddictApplicationDescriptor { ClientId = client.ClientId, ClientSecret = client.ClientSecret, DisplayName = updatedClient.DisplayName ?? client.DisplayName }; var redirectUris = updatedClient.RedirectUris ?? client.RedirectUris; foreach (var uri in redirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.RedirectUris.Add(new Uri(uri)); } var postLogoutUris = updatedClient.PostLogoutRedirectUris ?? client.PostLogoutRedirectUris; foreach (var uri in postLogoutUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); } var grantTypes = updatedClient.GrantTypes ?? client.GrantTypes; foreach (var grantType in grantTypes) { descriptor.Permissions.Add(grantType); } var scopes = updatedClient.Scopes ?? client.Scopes; foreach (var scope in scopes) { descriptor.Permissions.Add(scope); } await _applicationManager.UpdateAsync(application, descriptor); client.DisplayName = updatedClient.DisplayName ?? client.DisplayName; client.RedirectUris = redirectUris; client.PostLogoutRedirectUris = postLogoutUris; client.Scopes = scopes; client.GrantTypes = grantTypes; client.ClientType = updatedClient.ClientType ?? client.ClientType; client.ConsentType = updatedClient.ConsentType ?? client.ConsentType; client.Status = updatedClient.Status ?? client.Status; client.Description = updatedClient.Description ?? client.Description; client.UpdatedAt = DateTime.UtcNow; await _repository.UpdateAsync(client); _logger.LogInformation("Updated OAuth client {ClientId}", client.ClientId); return client; } public async Task DeleteClientAsync(long id) { var client = await _repository.GetByIdAsync(id); if (client == null) { throw new KeyNotFoundException($"Client with ID {id} not found"); } var application = await _applicationManager.FindByClientIdAsync(client.ClientId); if (application != null) { await _applicationManager.DeleteAsync(application); _logger.LogInformation("Deleted OpenIddict application for client {ClientId}", client.ClientId); } await _repository.DeleteAsync(client); _logger.LogInformation("Deleted OAuth client {ClientId}", client.ClientId); } public object GetClientOptions() { return new { clientTypes = new[] { new { value = "public", label = "Public (SPA, Mobile App)" }, new { value = "confidential", label = "Confidential (Server-side)" } }, consentTypes = new[] { new { value = "implicit", label = "Implicit" }, new { value = "explicit", label = "Explicit" }, new { value = "system", label = "System (Pre-authorized)" } }, grantTypes = new[] { new { value = "authorization_code", label = "Authorization Code" }, new { value = "client_credentials", label = "Client Credentials" }, new { value = "refresh_token", label = "Refresh Token" }, new { value = "password", label = "Resource Owner Password Credentials" } }, scopes = new[] { new { value = "openid", label = "OpenID Connect" }, new { value = "profile", label = "Profile" }, new { value = "email", label = "Email" }, new { value = "api", label = "API Access" }, new { value = "offline_access", label = "Offline Access" } }, statuses = new[] { new { value = "active", label = "Active" }, new { value = "inactive", label = "Inactive" }, new { value = "suspended", label = "Suspended" } } }; } private static string GenerateSecureSecret(int length = 32) { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var bytes = System.Security.Cryptography.RandomNumberGenerator.GetBytes(length); return new string(bytes.Select(b => chars[b % chars.Length]).ToArray()); } }