Implementing api key authentication scheme and validation. Added default value in dtos

This commit was merged in pull request #5.
This commit is contained in:
2026-01-17 21:05:20 +01:00
parent 01c86406dc
commit 56b14f60f1
8 changed files with 110 additions and 11 deletions

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Configuration;
public class ApiOptions
{
public int LookupCacheMinutes { get; set; } = 3;
}

View File

@@ -38,7 +38,7 @@ public class UserActionsController : Controller
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
await UsersRepository.UpdateAsync(user);
Cache.Remove(string.Format(UserAuthService.ValidationCacheKeyPattern, id));
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, id));
return NoContent();
}

View File

@@ -0,0 +1,71 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Shared;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
{
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
private readonly IMemoryCache MemoryCache;
public ApiKeySchemeHandler(
IOptionsMonitor<ApiKeySchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
DatabaseRepository<ApiKey> apiKeyRepository,
IMemoryCache memoryCache
) : base(options, logger, encoder)
{
ApiKeyRepository = apiKeyRepository;
MemoryCache = memoryCache;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authHeaderValue = Request.Headers.Authorization.FirstOrDefault() ?? null;
if (string.IsNullOrWhiteSpace(authHeaderValue))
return AuthenticateResult.NoResult();
if (authHeaderValue.Length > 32)
return AuthenticateResult.Fail("Invalid api key specified");
if (!MemoryCache.TryGetValue<ApiKeySession>(authHeaderValue, out var apiKey))
{
apiKey = await ApiKeyRepository
.Query()
.Where(x => x.Key == authHeaderValue)
.Select(x => new ApiKeySession(x.Permissions))
.FirstOrDefaultAsync();
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
MemoryCache.Set(authHeaderValue, apiKey, Options.LookupCacheTime);
}
else
{
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
}
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(
new ClaimsIdentity(
apiKey.Permissions.Select(x => new Claim(Permissions.ClaimType, x)).ToArray()
)
),
Scheme.Name
));
}
private record ApiKeySession(string[] Permissions);
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{
public TimeSpan LookupCacheTime { get; set; }
}

View File

@@ -20,7 +20,7 @@ public class UserAuthService
private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt";
public const string ValidationCacheKeyPattern = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{{0}}";
public const string CacheKeyPattern = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{{0}}";
public UserAuthService(
DatabaseRepository<User> userRepository,
@@ -88,8 +88,8 @@ public class UserAuthService
if (!int.TryParse(userIdString, out var userId))
return false;
var cacheKey = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{userId}";
var cacheKey = string.Format(CacheKeyPattern, userId);
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
{
user = await UserRepository

View File

@@ -5,8 +5,10 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moonlight.Api.Configuration;
using Moonlight.Api.Implementations;
using Moonlight.Api.Implementations.ApiKeyScheme;
using Moonlight.Api.Services;
namespace Moonlight.Api.Startup;
@@ -18,9 +20,17 @@ public partial class Startup
var oidcOptions = new OidcOptions();
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
var apiKeyOptions = new ApiOptions();
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
builder.Services.AddScoped<UserAuthService>();
builder.Services.AddAuthentication("Session")
builder.Services.AddAuthentication("Main")
.AddPolicyScheme("Main", null, options =>
{
options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
})
.AddCookie("Session", null, options =>
{
options.Events.OnSigningIn += async context =>
@@ -80,8 +90,14 @@ public partial class Startup
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.GetClaimsFromUserInfoEndpoint = true;
})
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, options =>
{
options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes);
});
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
}