using OpenIddict.Abstractions; using System.Security.Cryptography; using System.Text.Json; 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(string id); Task CreateClientAsync(CreateClientDto dto); Task GenerateSecretAsync(string id); Task UpdateClientAsync(string id, UpdateClientDto dto); Task DeleteClientAsync(string id); object GetClientOptions(); } public record CreateClientDto { public string ClientId { get; init; } = string.Empty; public string? ClientSecret { get; init; } public string DisplayName { get; init; } = string.Empty; public string[]? RedirectUris { get; init; } public string[]? PostLogoutRedirectUris { get; init; } public string[]? Scopes { get; init; } public string[]? GrantTypes { get; init; } public string? ClientType { get; init; } public string? ConsentType { get; init; } public string? Status { get; init; } public string? Description { get; init; } } public record UpdateClientDto { public string? DisplayName { get; init; } public string[]? RedirectUris { get; init; } public string[]? PostLogoutRedirectUris { get; init; } public string[]? Scopes { get; init; } public string[]? GrantTypes { get; init; } public string? ClientType { get; init; } public string? ConsentType { get; init; } public string? Status { get; init; } public string? Description { get; init; } } public class OAuthClientService : IOAuthClientService { private readonly IOpenIddictApplicationManager _applicationManager; private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; public OAuthClientService( IOpenIddictApplicationManager applicationManager, HttpClient httpClient, IConfiguration configuration, ILogger logger) { _applicationManager = applicationManager; _httpClient = httpClient; _configuration = configuration; _logger = logger; } public async Task<(IEnumerable Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null) { var applications = _applicationManager.ListAsync(); var clientList = new List(); await foreach (var application in applications) { var clientIdValue = await _applicationManager.GetClientIdAsync(application); var displayNameValue = await _applicationManager.GetDisplayNameAsync(application); if (!string.IsNullOrEmpty(displayName) && !displayNameValue.Contains(displayName, StringComparison.OrdinalIgnoreCase)) continue; if (!string.IsNullOrEmpty(clientId) && !clientIdValue.Contains(clientId, StringComparison.OrdinalIgnoreCase)) continue; var clientType = await _applicationManager.GetClientTypeAsync(application); var consentType = await _applicationManager.GetConsentTypeAsync(application); var permissions = await _applicationManager.GetPermissionsAsync(application); var redirectUris = await _applicationManager.GetRedirectUrisAsync(application); var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); clientList.Add(new { id = application, clientId = clientIdValue, displayName = displayNameValue, redirectUris = redirectUris.ToArray(), postLogoutRedirectUris = postLogoutRedirectUris.ToArray(), scopes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)), grantTypes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)) .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)), clientType = clientType?.ToString(), consentType = consentType?.ToString(), status = "active" }); } var sortedClients = clientList .OrderByDescending(c => (c as dynamic).clientId) .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); return (sortedClients, clientList.Count); } public async Task GetClientAsync(string id) { var application = await _applicationManager.FindByIdAsync(id); if (application == null) { return null; } var clientIdValue = await _applicationManager.GetClientIdAsync(application); var displayNameValue = await _applicationManager.GetDisplayNameAsync(application); var clientType = await _applicationManager.GetClientTypeAsync(application); var consentType = await _applicationManager.GetConsentTypeAsync(application); var permissions = await _applicationManager.GetPermissionsAsync(application); var redirectUris = await _applicationManager.GetRedirectUrisAsync(application); var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); return new { id = id, clientId = clientIdValue, displayName = displayNameValue, redirectUris = redirectUris.ToArray(), postLogoutRedirectUris = postLogoutRedirectUris.ToArray(), scopes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)), grantTypes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)) .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)), clientType = clientType?.ToString(), consentType = consentType?.ToString(), status = "active" }; } public async Task CreateClientAsync(CreateClientDto dto) { var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132"; var token = await GetAuthTokenAsync(); var clientSecret = string.IsNullOrEmpty(dto.ClientSecret) ? GenerateSecureSecret() : dto.ClientSecret; var requestBody = new { dto.ClientId, ClientSecret = clientSecret, dto.DisplayName, dto.RedirectUris, dto.PostLogoutRedirectUris, dto.Scopes, dto.GrantTypes, dto.ClientType, dto.ConsentType, dto.Status, dto.Description }; var request = new HttpRequestMessage(HttpMethod.Post, $"{authServiceUrl}/api/OAuthClients") { Content = new StringContent( System.Text.Json.JsonSerializer.Serialize(requestBody), System.Text.Encoding.UTF8, "application/json") }; request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); var result = System.Text.Json.JsonSerializer.Deserialize(content); _logger.LogInformation("Created OAuth client {ClientId}", dto.ClientId); return result!; } public async Task GenerateSecretAsync(string id) { var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132"; var token = await GetAuthTokenAsync(); var request = new HttpRequestMessage(HttpMethod.Post, $"{authServiceUrl}/api/OAuthClients/{id}/generate-secret"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); var result = System.Text.Json.JsonSerializer.Deserialize(content); _logger.LogInformation("Generated new secret for OAuth client {Id}", id); return result!; } public async Task UpdateClientAsync(string id, UpdateClientDto dto) { var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132"; var token = await GetAuthTokenAsync(); var requestBody = new { dto.DisplayName, dto.RedirectUris, dto.PostLogoutRedirectUris, dto.Scopes, dto.GrantTypes, dto.ClientType, dto.ConsentType, dto.Status, dto.Description }; var request = new HttpRequestMessage(HttpMethod.Put, $"{authServiceUrl}/api/OAuthClients/{id}") { Content = new StringContent( System.Text.Json.JsonSerializer.Serialize(requestBody), System.Text.Encoding.UTF8, "application/json") }; request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); _logger.LogInformation("Updated OAuth client {Id}", id); return new { message = "Client updated successfully" }; } public async Task DeleteClientAsync(string id) { var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132"; var token = await GetAuthTokenAsync(); var request = new HttpRequestMessage(HttpMethod.Delete, $"{authServiceUrl}/api/OAuthClients/{id}"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); _logger.LogInformation("Deleted OAuth client {Id}", id); } 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 async Task GetAuthTokenAsync() { var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132"; var clientId = _configuration["AuthService:ClientId"] ?? "console-client"; var clientSecret = _configuration["AuthService:ClientSecret"] ?? "console-secret"; var requestBody = new Dictionary { { "grant_type", "client_credentials" }, { "client_id", clientId }, { "client_secret", clientSecret }, { "scope", "api" } }; var request = new HttpRequestMessage(HttpMethod.Post, $"{authServiceUrl}/connect/token") { Content = new FormUrlEncodedContent(requestBody) }; var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); var tokenResponse = System.Text.Json.JsonSerializer.Deserialize>(content); return tokenResponse!["access_token"].GetString() ?? string.Empty; } private static string GenerateSecureSecret(int length = 32) { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var bytes = RandomNumberGenerator.GetBytes(length); return new string(bytes.Select(b => chars[b % chars.Length]).ToArray()); } }