302 lines
11 KiB
C#
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");
|
|
}
|
|
}
|