296 lines
11 KiB
C#
296 lines
11 KiB
C#
using MemberCenter.Api.Authorization;
|
|
using MemberCenter.Api.Contracts;
|
|
using MemberCenter.Application.Abstractions;
|
|
using MemberCenter.Application.Constants;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using OpenIddict.Abstractions;
|
|
using System.Text.Json;
|
|
|
|
namespace MemberCenter.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("admin/oauth-clients")]
|
|
[Authorize(Policy = "Admin")]
|
|
[AdminPermission(AdminPermissions.OAuthClients)]
|
|
public class AdminOAuthClientsController : ControllerBase
|
|
{
|
|
private readonly IOpenIddictApplicationManager _applicationManager;
|
|
private readonly IAuthResourceRegistryService _authResourceRegistry;
|
|
|
|
public AdminOAuthClientsController(
|
|
IOpenIddictApplicationManager applicationManager,
|
|
IAuthResourceRegistryService authResourceRegistry)
|
|
{
|
|
_applicationManager = applicationManager;
|
|
_authResourceRegistry = authResourceRegistry;
|
|
}
|
|
|
|
[HttpGet]
|
|
[AdminPermission(AdminPermissions.OAuthClientsIndex)]
|
|
public async Task<IActionResult> List()
|
|
{
|
|
var results = new List<object>();
|
|
await foreach (var application in _applicationManager.ListAsync())
|
|
{
|
|
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),
|
|
redirect_uris = await _applicationManager.GetRedirectUrisAsync(application),
|
|
properties = await _applicationManager.GetPropertiesAsync(application)
|
|
});
|
|
}
|
|
|
|
return Ok(results);
|
|
}
|
|
|
|
[HttpPost]
|
|
[AdminPermission(AdminPermissions.OAuthClientsCreate)]
|
|
public async Task<IActionResult> Create([FromBody] OAuthClientRequest request)
|
|
{
|
|
if (!IsValidUsage(request.Usage))
|
|
{
|
|
return BadRequest("usage must be tenant_api, send_api, web_login, webhook_outbound, platform_service, or file_api.");
|
|
}
|
|
|
|
if (!IsTenantOptionalUsage(request.Usage) && (!request.TenantId.HasValue || request.TenantId.Value == Guid.Empty))
|
|
{
|
|
return BadRequest("tenant_id is required for this usage.");
|
|
}
|
|
|
|
if (RequiresClientCredentials(request.Usage)
|
|
&& !string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return BadRequest("client_type must be confidential for this usage.");
|
|
}
|
|
|
|
var (redirectUris, redirectUriError) = NormalizeRedirectUris(request.RedirectUris);
|
|
if (!string.IsNullOrWhiteSpace(redirectUriError))
|
|
{
|
|
return BadRequest(redirectUriError);
|
|
}
|
|
if (UsesAuthorizationCodeFlow(request.Usage) && redirectUris.Count == 0)
|
|
{
|
|
return BadRequest("redirect_uris is required for web_login or webhook_outbound usage.");
|
|
}
|
|
|
|
var descriptor = new OpenIddictApplicationDescriptor
|
|
{
|
|
ClientId = Guid.NewGuid().ToString("N"),
|
|
DisplayName = request.Name,
|
|
ClientType = request.ClientType
|
|
};
|
|
await ApplyPermissionsAsync(descriptor, request.Usage);
|
|
|
|
foreach (var uri in redirectUris)
|
|
{
|
|
descriptor.RedirectUris.Add(new Uri(uri));
|
|
}
|
|
|
|
if (!IsTenantOptionalUsage(request.Usage) && request.TenantId.HasValue)
|
|
{
|
|
descriptor.Properties["tenant_id"] = JsonSerializer.SerializeToElement(request.TenantId.Value.ToString());
|
|
}
|
|
descriptor.Properties["usage"] = JsonSerializer.SerializeToElement(request.Usage);
|
|
|
|
await _applicationManager.CreateAsync(descriptor);
|
|
|
|
return Created("/admin/oauth-clients", new
|
|
{
|
|
descriptor.ClientId,
|
|
descriptor.DisplayName,
|
|
descriptor.ClientType,
|
|
redirect_uris = descriptor.RedirectUris.Select(u => u.ToString())
|
|
});
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
[AdminPermission(AdminPermissions.OAuthClientsIndex)]
|
|
public async Task<IActionResult> Get(string id)
|
|
{
|
|
var app = await _applicationManager.FindByIdAsync(id);
|
|
if (app is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
return Ok(new
|
|
{
|
|
id,
|
|
name = await _applicationManager.GetDisplayNameAsync(app),
|
|
client_id = await _applicationManager.GetClientIdAsync(app),
|
|
client_type = await _applicationManager.GetClientTypeAsync(app),
|
|
redirect_uris = await _applicationManager.GetRedirectUrisAsync(app),
|
|
properties = await _applicationManager.GetPropertiesAsync(app)
|
|
});
|
|
}
|
|
|
|
[HttpPut("{id}")]
|
|
[AdminPermission(AdminPermissions.OAuthClientsEdit)]
|
|
public async Task<IActionResult> Update(string id, [FromBody] OAuthClientRequest request)
|
|
{
|
|
if (!IsValidUsage(request.Usage))
|
|
{
|
|
return BadRequest("usage must be tenant_api, send_api, web_login, webhook_outbound, platform_service, or file_api.");
|
|
}
|
|
|
|
if (!IsTenantOptionalUsage(request.Usage) && (!request.TenantId.HasValue || request.TenantId.Value == Guid.Empty))
|
|
{
|
|
return BadRequest("tenant_id is required for this usage.");
|
|
}
|
|
|
|
if (RequiresClientCredentials(request.Usage)
|
|
&& !string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return BadRequest("client_type must be confidential for this usage.");
|
|
}
|
|
|
|
var (redirectUris, redirectUriError) = NormalizeRedirectUris(request.RedirectUris);
|
|
if (!string.IsNullOrWhiteSpace(redirectUriError))
|
|
{
|
|
return BadRequest(redirectUriError);
|
|
}
|
|
if (UsesAuthorizationCodeFlow(request.Usage) && redirectUris.Count == 0)
|
|
{
|
|
return BadRequest("redirect_uris is required for web_login or webhook_outbound usage.");
|
|
}
|
|
|
|
var app = await _applicationManager.FindByIdAsync(id);
|
|
if (app is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
var descriptor = new OpenIddictApplicationDescriptor();
|
|
await _applicationManager.PopulateAsync(descriptor, app);
|
|
|
|
descriptor.DisplayName = request.Name;
|
|
descriptor.ClientType = request.ClientType;
|
|
if (string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
descriptor.ClientSecret = null;
|
|
}
|
|
|
|
await ApplyPermissionsAsync(descriptor, request.Usage);
|
|
descriptor.RedirectUris.Clear();
|
|
foreach (var uri in redirectUris)
|
|
{
|
|
descriptor.RedirectUris.Add(new Uri(uri));
|
|
}
|
|
if (!IsTenantOptionalUsage(request.Usage) && request.TenantId.HasValue)
|
|
{
|
|
descriptor.Properties["tenant_id"] = JsonSerializer.SerializeToElement(request.TenantId.Value.ToString());
|
|
}
|
|
else
|
|
{
|
|
descriptor.Properties.Remove("tenant_id");
|
|
}
|
|
descriptor.Properties["usage"] = JsonSerializer.SerializeToElement(request.Usage);
|
|
|
|
await _applicationManager.UpdateAsync(app, descriptor);
|
|
|
|
return Ok(new
|
|
{
|
|
id,
|
|
descriptor.DisplayName,
|
|
descriptor.ClientType,
|
|
redirect_uris = descriptor.RedirectUris.Select(u => u.ToString())
|
|
});
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
[AdminPermission(AdminPermissions.OAuthClientsDelete)]
|
|
public async Task<IActionResult> Delete(string id)
|
|
{
|
|
var app = await _applicationManager.FindByIdAsync(id);
|
|
if (app is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
await _applicationManager.DeleteAsync(app);
|
|
return NoContent();
|
|
}
|
|
|
|
private static bool IsValidUsage(string usage)
|
|
{
|
|
return string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "send_api", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "webhook_outbound", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "file_api", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static bool IsTenantOptionalUsage(string usage)
|
|
{
|
|
return string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static bool RequiresClientCredentials(string usage)
|
|
{
|
|
return string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "send_api", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "file_api", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static bool UsesAuthorizationCodeFlow(string usage)
|
|
{
|
|
return string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(usage, "webhook_outbound", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static (List<string> Uris, string? Error) NormalizeRedirectUris(List<string>? redirectUris)
|
|
{
|
|
if (redirectUris is null || redirectUris.Count == 0)
|
|
{
|
|
return ([], null);
|
|
}
|
|
|
|
var values = redirectUris
|
|
.Where(uri => !string.IsNullOrWhiteSpace(uri))
|
|
.Select(uri => uri.Trim())
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
foreach (var uri in values)
|
|
{
|
|
if (!Uri.TryCreate(uri, UriKind.Absolute, out _))
|
|
{
|
|
return ([], "redirect_uris must contain absolute URIs only.");
|
|
}
|
|
}
|
|
|
|
return (values, null);
|
|
}
|
|
|
|
private async Task ApplyPermissionsAsync(OpenIddictApplicationDescriptor descriptor, string usage)
|
|
{
|
|
descriptor.Permissions.Clear();
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
|
|
|
|
if (UsesAuthorizationCodeFlow(usage))
|
|
{
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials);
|
|
}
|
|
|
|
var scopes = await _authResourceRegistry.GetAllowedScopesForUsageAsync(usage, HttpContext.RequestAborted);
|
|
foreach (var scope in scopes)
|
|
{
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
|
|
}
|
|
}
|
|
}
|