member_center/src/MemberCenter.Web/Controllers/Admin/OAuthClientsController.cs

302 lines
11 KiB
C#

using MemberCenter.Web.Models.Admin;
using MemberCenter.Application.Abstractions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
namespace MemberCenter.Web.Controllers.Admin;
[Authorize(Policy = "Admin")]
[Route("admin/oauth-clients")]
public class OAuthClientsController : Controller
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly ITenantService _tenantService;
public OAuthClientsController(
IOpenIddictApplicationManager applicationManager,
ITenantService tenantService)
{
_applicationManager = applicationManager;
_tenantService = tenantService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var results = new List<object>();
await foreach (var application in _applicationManager.ListAsync())
{
var properties = await _applicationManager.GetPropertiesAsync(application);
var usage = properties.TryGetValue("usage", out var usageElement)
? usageElement.GetString()
: "tenant_api";
results.Add(new
{
id = await _applicationManager.GetIdAsync(application),
name = await _applicationManager.GetDisplayNameAsync(application),
client_id = await _applicationManager.GetClientIdAsync(application),
client_type = await _applicationManager.GetClientTypeAsync(application),
usage = usage,
redirect_uris = await _applicationManager.GetRedirectUrisAsync(application)
});
}
return View(results);
}
[HttpGet("create")]
public async Task<IActionResult> Create()
{
var tenants = await _tenantService.ListAsync();
return View(new OAuthClientFormViewModel
{
Tenants = tenants
});
}
[HttpPost("create")]
public async Task<IActionResult> Create(OAuthClientFormViewModel model)
{
if (!IsValidUsage(model.Usage))
{
ModelState.AddModelError(nameof(model.Usage), "Usage must be tenant_api, webhook_outbound, or platform_service.");
}
if (!IsTenantOptionalUsage(model.Usage) && (!model.TenantId.HasValue || model.TenantId.Value == Guid.Empty))
{
ModelState.AddModelError(nameof(model.TenantId), "Tenant is required for this usage.");
}
if (RequiresClientCredentials(model.Usage)
&& !string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
{
ModelState.AddModelError(nameof(model.ClientType), "Client type must be confidential for this usage.");
}
if (!ModelState.IsValid)
{
model.Tenants = await _tenantService.ListAsync();
return View(model);
}
var clientId = Guid.NewGuid().ToString("N");
var clientSecret = model.ClientType == "confidential"
? Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32))
: null;
var descriptor = BuildDescriptor(clientId, model.Name, model.ClientType, model.Usage);
if (!string.IsNullOrWhiteSpace(clientSecret))
{
descriptor.ClientSecret = clientSecret;
}
foreach (var uri in model.RedirectUris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
descriptor.RedirectUris.Add(new Uri(uri));
}
if (!IsTenantOptionalUsage(model.Usage) && model.TenantId.HasValue)
{
descriptor.Properties["tenant_id"] = System.Text.Json.JsonSerializer.SerializeToElement(model.TenantId.Value.ToString());
}
descriptor.Properties["usage"] = System.Text.Json.JsonSerializer.SerializeToElement(model.Usage);
await _applicationManager.CreateAsync(descriptor);
TempData["CreatedClientId"] = clientId;
if (!string.IsNullOrWhiteSpace(clientSecret))
{
TempData["CreatedClientSecret"] = clientSecret;
}
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
var app = await _applicationManager.FindByIdAsync(id);
if (app is null)
{
return NotFound();
}
var redirectUris = await _applicationManager.GetRedirectUrisAsync(app);
var properties = await _applicationManager.GetPropertiesAsync(app);
var tenantId = properties.TryGetValue("tenant_id", out var value) ? value.GetString() : string.Empty;
var usage = properties.TryGetValue("usage", out var usageValue) ? usageValue.GetString() : "tenant_api";
var tenants = await _tenantService.ListAsync();
return View(new OAuthClientFormViewModel
{
TenantId = Guid.TryParse(tenantId, out var parsed) ? parsed : null,
Name = await _applicationManager.GetDisplayNameAsync(app) ?? string.Empty,
ClientType = await _applicationManager.GetClientTypeAsync(app) ?? "public",
Usage = string.IsNullOrWhiteSpace(usage) ? "tenant_api" : usage,
RedirectUris = string.Join(",", redirectUris.Select(u => u.ToString())),
Tenants = tenants
});
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, OAuthClientFormViewModel model)
{
if (!IsValidUsage(model.Usage))
{
ModelState.AddModelError(nameof(model.Usage), "Usage must be tenant_api, webhook_outbound, or platform_service.");
}
if (!IsTenantOptionalUsage(model.Usage) && (!model.TenantId.HasValue || model.TenantId.Value == Guid.Empty))
{
ModelState.AddModelError(nameof(model.TenantId), "Tenant is required for this usage.");
}
if (RequiresClientCredentials(model.Usage)
&& !string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
{
ModelState.AddModelError(nameof(model.ClientType), "Client type must be confidential for this usage.");
}
if (!ModelState.IsValid)
{
model.Tenants = await _tenantService.ListAsync();
return View(model);
}
var app = await _applicationManager.FindByIdAsync(id);
if (app is null)
{
return NotFound();
}
var descriptor = new OpenIddictApplicationDescriptor();
await _applicationManager.PopulateAsync(descriptor, app);
descriptor.DisplayName = model.Name;
descriptor.ClientType = model.ClientType;
ApplyPermissions(descriptor, model.Usage);
descriptor.RedirectUris.Clear();
foreach (var uri in model.RedirectUris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
descriptor.RedirectUris.Add(new Uri(uri));
}
if (!IsTenantOptionalUsage(model.Usage) && model.TenantId.HasValue)
{
descriptor.Properties["tenant_id"] = System.Text.Json.JsonSerializer.SerializeToElement(model.TenantId.Value.ToString());
}
else
{
descriptor.Properties.Remove("tenant_id");
}
descriptor.Properties["usage"] = System.Text.Json.JsonSerializer.SerializeToElement(model.Usage);
await _applicationManager.UpdateAsync(app, descriptor);
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
var app = await _applicationManager.FindByIdAsync(id);
if (app is null)
{
return NotFound();
}
await _applicationManager.DeleteAsync(app);
return RedirectToAction("Index");
}
[HttpPost("rotate-secret/{id}")]
public async Task<IActionResult> RotateSecret(string id)
{
var app = await _applicationManager.FindByIdAsync(id);
if (app is null)
{
return NotFound();
}
var clientType = await _applicationManager.GetClientTypeAsync(app);
if (!string.Equals(clientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Only confidential clients have secrets.");
}
var descriptor = new OpenIddictApplicationDescriptor();
await _applicationManager.PopulateAsync(descriptor, app);
var newSecret = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32));
descriptor.ClientSecret = newSecret;
await _applicationManager.UpdateAsync(app, descriptor);
TempData["RotatedClientId"] = await _applicationManager.GetClientIdAsync(app);
TempData["RotatedClientSecret"] = newSecret;
return RedirectToAction("Index");
}
private static bool IsValidUsage(string usage)
{
return string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase)
|| string.Equals(usage, "webhook_outbound", StringComparison.OrdinalIgnoreCase)
|| string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase);
}
private static bool IsTenantOptionalUsage(string usage)
{
return string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase);
}
private static bool RequiresClientCredentials(string usage)
{
return string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
|| string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase);
}
private static OpenIddictApplicationDescriptor BuildDescriptor(string clientId, string name, string clientType, string usage)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = clientId,
DisplayName = name,
ClientType = clientType
};
ApplyPermissions(descriptor, usage);
return descriptor;
}
private static void ApplyPermissions(OpenIddictApplicationDescriptor descriptor, string usage)
{
descriptor.Permissions.Clear();
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
if (string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
|| string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase))
{
descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials);
if (string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase))
{
descriptor.Permissions.Add("scp:newsletter:events.write.global");
}
else
{
descriptor.Permissions.Add("scp:newsletter:events.write");
}
return;
}
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Scopes.Email);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Scopes.Profile);
descriptor.Permissions.Add("scp:openid");
}
}