82 lines
3.0 KiB
C#
82 lines
3.0 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.Extensions.Options;
|
|
using MxGateway.Server.Configuration;
|
|
using MxGateway.Server.Security.Authentication;
|
|
using MxGateway.Server.Security.Authorization;
|
|
|
|
namespace MxGateway.Server.Dashboard;
|
|
|
|
public sealed class DashboardAuthenticator(
|
|
IApiKeyVerifier apiKeyVerifier,
|
|
IOptions<GatewayOptions> options) : IDashboardAuthenticator
|
|
{
|
|
private const string GenericFailureMessage = "The API key is invalid or is not authorized for dashboard access.";
|
|
|
|
public async Task<DashboardAuthenticationResult> AuthenticateAsync(
|
|
string? apiKey,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (options.Value.Authentication.Mode == AuthenticationMode.Disabled)
|
|
{
|
|
return DashboardAuthenticationResult.Success(CreatePrincipal(new ApiKeyIdentity(
|
|
KeyId: "authentication-disabled",
|
|
KeyPrefix: "authentication-disabled",
|
|
DisplayName: "Authentication Disabled",
|
|
Scopes: new HashSet<string>([GatewayScopes.Admin], StringComparer.Ordinal))));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(apiKey))
|
|
{
|
|
return DashboardAuthenticationResult.Fail(GenericFailureMessage);
|
|
}
|
|
|
|
ApiKeyVerificationResult verificationResult = await apiKeyVerifier
|
|
.VerifyAsync(FormatAuthorizationHeader(apiKey), cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (!verificationResult.Succeeded || verificationResult.Identity is null)
|
|
{
|
|
return DashboardAuthenticationResult.Fail(GenericFailureMessage);
|
|
}
|
|
|
|
if (options.Value.Dashboard.RequireAdminScope
|
|
&& !verificationResult.Identity.Scopes.Contains(GatewayScopes.Admin))
|
|
{
|
|
return DashboardAuthenticationResult.Fail(GenericFailureMessage);
|
|
}
|
|
|
|
return DashboardAuthenticationResult.Success(CreatePrincipal(verificationResult.Identity));
|
|
}
|
|
|
|
private static string FormatAuthorizationHeader(string apiKey)
|
|
{
|
|
string trimmedApiKey = apiKey.Trim();
|
|
|
|
return trimmedApiKey.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
|
? trimmedApiKey
|
|
: $"Bearer {trimmedApiKey}";
|
|
}
|
|
|
|
private static ClaimsPrincipal CreatePrincipal(ApiKeyIdentity identity)
|
|
{
|
|
List<Claim> claims =
|
|
[
|
|
new Claim(ClaimTypes.NameIdentifier, identity.KeyId),
|
|
new Claim(ClaimTypes.Name, identity.DisplayName),
|
|
new Claim(DashboardAuthenticationDefaults.KeyPrefixClaimType, identity.KeyPrefix)
|
|
];
|
|
|
|
claims.AddRange(identity.Scopes.Select(scope => new Claim(
|
|
DashboardAuthenticationDefaults.ScopeClaimType,
|
|
scope)));
|
|
|
|
ClaimsIdentity claimsIdentity = new(
|
|
claims,
|
|
DashboardAuthenticationDefaults.AuthenticationScheme,
|
|
ClaimTypes.Name,
|
|
DashboardAuthenticationDefaults.ScopeClaimType);
|
|
|
|
return new ClaimsPrincipal(claimsIdentity);
|
|
}
|
|
}
|