456 lines
15 KiB
C#
456 lines
15 KiB
C#
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<OAuthClientsController> _logger;
|
|
|
|
public OAuthClientsController(
|
|
ApplicationDbContext context,
|
|
IOpenIddictApplicationManager applicationManager,
|
|
ILogger<OAuthClientsController> logger)
|
|
{
|
|
_context = context;
|
|
_applicationManager = applicationManager;
|
|
_logger = logger;
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult<object>> 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<object> 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<ActionResult<OAuthApplication>> 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<ActionResult<object>> 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<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
|
{
|
|
descriptor.RedirectUris.Add(new Uri(uri));
|
|
}
|
|
|
|
foreach (var uri in (dto.PostLogoutRedirectUris ?? Array.Empty<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
|
{
|
|
descriptor.PostLogoutRedirectUris.Add(new Uri(uri));
|
|
}
|
|
|
|
foreach (var grantType in dto.GrantTypes ?? Array.Empty<string>())
|
|
{
|
|
descriptor.Permissions.Add(grantType);
|
|
}
|
|
|
|
foreach (var scope in dto.Scopes ?? Array.Empty<string>())
|
|
{
|
|
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<string>(),
|
|
PostLogoutRedirectUris = dto.PostLogoutRedirectUris ?? Array.Empty<string>(),
|
|
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<ActionResult<object>> 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<ActionResult<object>> 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<IActionResult> 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<IActionResult> 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; }
|
|
}
|