using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using System.Security.Cryptography; namespace Fengling.AuthService.Controllers; [ApiController] [Route("api/[controller]")] [Authorize] public class OAuthClientsController : ControllerBase { private readonly IOpenIddictApplicationManager _applicationManager; private readonly ILogger _logger; public OAuthClientsController( IOpenIddictApplicationManager applicationManager, ILogger logger) { _applicationManager = applicationManager; _logger = logger; } [HttpGet] public async Task> GetClients( [FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? displayName = null, [FromQuery] string? clientId = 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 redirectUrisStrings = await _applicationManager.GetRedirectUrisAsync(application); var postLogoutRedirectUrisStrings = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); var redirectUris = redirectUrisStrings; var postLogoutRedirectUris = postLogoutRedirectUrisStrings; clientList.Add(new { id = application, clientId = clientIdValue, displayName = displayNameValue, redirectUris = redirectUris.Select(u => u.ToString()).ToArray(), postLogoutRedirectUris = postLogoutRedirectUris.Select(u => u.ToString()).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", permissions }); } var sortedClients = clientList .OrderByDescending(c => (c as dynamic).clientId) .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); return Ok(new { items = sortedClients, totalCount = clientList.Count, page, pageSize }); } [HttpGet("options")] public ActionResult GetClientOptions() { return Ok(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" } } }); } [HttpGet("{id}")] public async Task> GetClient(string id) { var application = await _applicationManager.FindByIdAsync(id); if (application == null) { return NotFound(); } 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 Ok(new { id = id, clientId = clientIdValue, displayName = displayNameValue, redirectUris = redirectUris.Select(u => u.ToString()).ToArray(), postLogoutRedirectUris = postLogoutRedirectUris.Select(u => u.ToString()).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", permissions }); } [HttpPost] public async Task> CreateClient([FromBody] CreateOAuthClientDto dto) { var existingClient = await _applicationManager.FindByClientIdAsync(dto.ClientId); if (existingClient != null) { return BadRequest(new { message = "Client ID 已存在", clientId = dto.ClientId }); } var clientSecret = string.IsNullOrEmpty(dto.ClientSecret) ? GenerateSecureSecret() : dto.ClientSecret; var descriptor = new OpenIddictApplicationDescriptor { ClientId = dto.ClientId, ClientSecret = clientSecret, DisplayName = dto.DisplayName, ClientType = string.IsNullOrEmpty(dto.ClientType) ? "confidential" : dto.ClientType, ConsentType = string.IsNullOrEmpty(dto.ConsentType) ? "explicit" : dto.ConsentType, }; descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection); foreach (var uri in (dto.RedirectUris ?? Array.Empty()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.RedirectUris.Add(new Uri(uri)); } foreach (var uri in (dto.PostLogoutRedirectUris ?? Array.Empty()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); } foreach (var grantType in dto.GrantTypes ?? Array.Empty()) { descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.GrantType + grantType); } foreach (var scope in dto.Scopes ?? Array.Empty()) { descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope); } var application = await _applicationManager.CreateAsync(descriptor); var applicationId = await _applicationManager.GetIdAsync(application); return CreatedAtAction(nameof(GetClient), new { id = applicationId }, new { id = applicationId, clientId = dto.ClientId, clientSecret = clientSecret, displayName = dto.DisplayName, status = "active" }); } [HttpPost("{id}/generate-secret")] public async Task> GenerateSecret(string id) { var application = await _applicationManager.FindByIdAsync(id); if (application == null) { return NotFound(); } 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); var newSecret = GenerateSecureSecret(); var descriptor = new OpenIddictApplicationDescriptor { ClientId = clientIdValue, ClientSecret = newSecret, DisplayName = displayNameValue, ClientType = clientType, ConsentType = consentType, }; foreach (var permission in permissions) { descriptor.Permissions.Add(permission); } foreach (var uriString in redirectUris) { if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) { descriptor.RedirectUris.Add(uri); } } foreach (var uriString in postLogoutRedirectUris) { if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) { descriptor.PostLogoutRedirectUris.Add(uri); } } await _applicationManager.UpdateAsync(application, descriptor); return Ok(new { clientId = clientIdValue, clientSecret = newSecret, message = "新密钥已生成,请妥善保管,刷新后将无法再次查看" }); } [HttpDelete("{id}")] public async Task DeleteClient(string id) { var application = await _applicationManager.FindByIdAsync(id); if (application == null) { return NotFound(); } var clientIdValue = await _applicationManager.GetClientIdAsync(application); try { await _applicationManager.DeleteAsync(application); _logger.LogInformation("Deleted OpenIddict application {ClientId}", clientIdValue); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to delete OpenIddict application for client {ClientId}", clientIdValue); return BadRequest(new { message = ex.Message }); } return NoContent(); } [HttpPut("{id}")] public async Task UpdateClient(string id, [FromBody] UpdateOAuthClientDto dto) { var application = await _applicationManager.FindByIdAsync(id); if (application == null) { return NotFound(); } 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 redirectUris = await _applicationManager.GetRedirectUrisAsync(application); var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); var existingPermissions = await _applicationManager.GetPermissionsAsync(application); var descriptor = new OpenIddictApplicationDescriptor { ClientId = clientIdValue, DisplayName = dto.DisplayName ?? displayNameValue, ClientType = string.IsNullOrEmpty(dto.ClientType) ? clientType?.ToString() : dto.ClientType, ConsentType = string.IsNullOrEmpty(dto.ConsentType) ? consentType?.ToString() : dto.ConsentType, }; descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection); if (dto.RedirectUris != null) { foreach (var uri in dto.RedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.RedirectUris.Add(new Uri(uri)); } } else { foreach (var uriString in redirectUris) { if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) { descriptor.RedirectUris.Add(uri); } } } if (dto.PostLogoutRedirectUris != null) { foreach (var uri in dto.PostLogoutRedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); } } else { foreach (var uriString in postLogoutRedirectUris) { if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) { descriptor.PostLogoutRedirectUris.Add(uri); } } } var grantTypes = dto.GrantTypes ?? existingPermissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)) .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)); var scopes = dto.Scopes ?? existingPermissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)); foreach (var grantType in grantTypes) { descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.GrantType + grantType); } foreach (var scope in scopes) { descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope); } await _applicationManager.UpdateAsync(application, descriptor); return NoContent(); } 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()); } } public class CreateOAuthClientDto { public string ClientId { get; set; } = string.Empty; public string? ClientSecret { get; set; } public string DisplayName { get; set; } = string.Empty; public string[]? RedirectUris { get; set; } public string[]? PostLogoutRedirectUris { get; set; } public string[]? Scopes { get; set; } public string[]? GrantTypes { get; set; } public string? ClientType { get; set; } public string? ConsentType { get; set; } public string? Status { get; set; } public string? Description { get; set; } } public class UpdateOAuthClientDto { public string? DisplayName { get; set; } public string[]? RedirectUris { get; set; } public string[]? PostLogoutRedirectUris { get; set; } public string[]? Scopes { get; set; } public string[]? GrantTypes { get; set; } public string? ClientType { get; set; } public string? ConsentType { get; set; } public string? Status { get; set; } public string? Description { get; set; } }