using Fengling.AuthService.Data; using Fengling.AuthService.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OpenIddict.Abstractions; using System.Security.Claims; using System.Security.Cryptography; namespace Fengling.AuthService.Controllers; [ApiController] [Route("api/[controller]")] [Authorize] public class OAuthClientsController : ControllerBase { private readonly ApplicationDbContext _context; private readonly IOpenIddictApplicationManager _applicationManager; private readonly ILogger _logger; public OAuthClientsController( ApplicationDbContext context, IOpenIddictApplicationManager applicationManager, ILogger logger) { _context = context; _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, [FromQuery] string? status = null) { var query = _context.OAuthApplications.AsQueryable(); if (!string.IsNullOrEmpty(displayName)) { query = query.Where(c => c.DisplayName.Contains(displayName)); } if (!string.IsNullOrEmpty(clientId)) { query = query.Where(c => c.ClientId.Contains(clientId)); } if (!string.IsNullOrEmpty(status)) { query = query.Where(c => c.Status == status); } var totalCount = await query.CountAsync(); var clients = await query .OrderByDescending(c => c.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); var result = clients.Select(c => new { id = c.Id, clientId = c.ClientId, displayName = c.DisplayName, redirectUris = c.RedirectUris, postLogoutRedirectUris = c.PostLogoutRedirectUris, scopes = c.Scopes, grantTypes = c.GrantTypes, clientType = c.ClientType, consentType = c.ConsentType, status = c.Status, description = c.Description, createdAt = c.CreatedAt, updatedAt = c.UpdatedAt, }); return Ok(new { items = result, totalCount, 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(long id) { var client = await _context.OAuthApplications.FindAsync(id); if (client == null) { return NotFound(); } return Ok(new { id = client.Id, clientId = client.ClientId, displayName = client.DisplayName, redirectUris = client.RedirectUris, postLogoutRedirectUris = client.PostLogoutRedirectUris, scopes = client.Scopes, grantTypes = client.GrantTypes, clientType = client.ClientType, consentType = client.ConsentType, status = client.Status, description = client.Description, createdAt = client.CreatedAt, updatedAt = client.UpdatedAt, }); } [HttpPost] public async Task> CreateClient([FromBody] CreateOAuthClientDto dto) { var existingClient = await _context.OAuthApplications.FirstOrDefaultAsync(c => c.ClientId == 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 }; 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(grantType); } foreach (var scope in dto.Scopes ?? Array.Empty()) { descriptor.Permissions.Add(scope); } await _applicationManager.CreateAsync(descriptor); var client = new OAuthApplication { ClientId = dto.ClientId, ClientSecret = clientSecret, DisplayName = dto.DisplayName, RedirectUris = dto.RedirectUris ?? Array.Empty(), PostLogoutRedirectUris = dto.PostLogoutRedirectUris ?? Array.Empty(), Scopes = dto.Scopes ?? new[] { "openid", "profile", "email", "api" }, GrantTypes = dto.GrantTypes ?? new[] { "authorization_code", "refresh_token" }, ClientType = dto.ClientType ?? "confidential", ConsentType = dto.ConsentType ?? "explicit", Status = dto.Status ?? "active", Description = dto.Description, CreatedAt = DateTime.UtcNow, }; _context.OAuthApplications.Add(client); await _context.SaveChangesAsync(); await CreateAuditLog("oauth", "create", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(dto)); return CreatedAtAction(nameof(GetClient), new { id = client.Id }, new { client.Id, client.ClientId, client.ClientSecret, client.DisplayName, client.Status, client.CreatedAt }); } [HttpPost("{id}/generate-secret")] public async Task> GenerateSecret(long id) { var client = await _context.OAuthApplications.FindAsync(id); if (client == null) { return NotFound(); } var newSecret = GenerateSecureSecret(); client.ClientSecret = newSecret; client.UpdatedAt = DateTime.UtcNow; var application = await _applicationManager.FindByClientIdAsync(client.ClientId); if (application != null) { var descriptor = new OpenIddictApplicationDescriptor { ClientId = client.ClientId, ClientSecret = newSecret, DisplayName = client.DisplayName }; await _applicationManager.UpdateAsync(application, descriptor); } await _context.SaveChangesAsync(); await CreateAuditLog("oauth", "generate_secret", "OAuthClient", client.Id, client.DisplayName, "[REDACTED]", "[REDACTED]"); return Ok(new { clientId = client.ClientId, clientSecret = newSecret, message = "新密钥已生成,请妥善保管,刷新后将无法再次查看" }); } [HttpPost("{id}/toggle-status")] public async Task> ToggleStatus(long id) { var client = await _context.OAuthApplications.FindAsync(id); if (client == null) { return NotFound(); } var oldStatus = client.Status; client.Status = client.Status == "active" ? "inactive" : "active"; client.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); await CreateAuditLog("oauth", "toggle_status", "OAuthClient", client.Id, client.DisplayName, oldStatus, client.Status); return Ok(new { clientId = client.ClientId, oldStatus, newStatus = client.Status, message = $"客户端状态已从 {oldStatus} 更改为 {client.Status}" }); } [HttpPut("{id}")] public async Task UpdateClient(long id, [FromBody] UpdateOAuthClientDto dto) { var client = await _context.OAuthApplications.FindAsync(id); if (client == null) { return NotFound(); } var application = await _applicationManager.FindByClientIdAsync(client.ClientId); if (application == null) { return NotFound(); } var descriptor = new OpenIddictApplicationDescriptor { ClientId = client.ClientId, ClientSecret = client.ClientSecret, DisplayName = dto.DisplayName ?? client.DisplayName }; var redirectUris = dto.RedirectUris ?? client.RedirectUris; foreach (var uri in redirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.RedirectUris.Add(new Uri(uri)); } var postLogoutUris = dto.PostLogoutRedirectUris ?? client.PostLogoutRedirectUris; foreach (var uri in postLogoutUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) { descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); } var grantTypes = dto.GrantTypes ?? client.GrantTypes; foreach (var grantType in grantTypes) { descriptor.Permissions.Add(grantType); } var scopes = dto.Scopes ?? client.Scopes; foreach (var scope in scopes) { descriptor.Permissions.Add(scope); } await _applicationManager.UpdateAsync(application, descriptor); client.DisplayName = dto.DisplayName ?? client.DisplayName; client.RedirectUris = redirectUris; client.PostLogoutRedirectUris = postLogoutUris; client.Scopes = scopes; client.GrantTypes = grantTypes; client.ClientType = dto.ClientType ?? client.ClientType; client.ConsentType = dto.ConsentType ?? client.ConsentType; client.Status = dto.Status ?? client.Status; client.Description = dto.Description ?? client.Description; client.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); await CreateAuditLog("oauth", "update", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(client)); return NoContent(); } [HttpDelete("{id}")] public async Task DeleteClient(long id) { var client = await _context.OAuthApplications.FindAsync(id); if (client == null) { return NotFound(); } try { var application = await _applicationManager.FindByClientIdAsync(client.ClientId); if (application != null) { await _applicationManager.DeleteAsync(application); _logger.LogInformation("Deleted OpenIddict application {ClientId}", client.ClientId); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to delete OpenIddict application for client {ClientId}", client.ClientId); } _context.OAuthApplications.Remove(client); await _context.SaveChangesAsync(); await CreateAuditLog("oauth", "delete", "OAuthClient", client.Id, client.DisplayName, SerializeToJson(client)); 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()); } private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null) { var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system"; var tenantId = User.FindFirstValue("TenantId"); var log = new AuditLog { Operator = userName, TenantId = tenantId, Operation = operation, Action = action, TargetType = targetType, TargetId = targetId, TargetName = targetName, IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", Status = "success", OldValue = oldValue, NewValue = newValue, }; _context.AuditLogs.Add(log); await _context.SaveChangesAsync(); } private static string SerializeToJson(object obj) { return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions { WriteIndented = false }); } } 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; } }