feat/ApiKeys #5
6
Moonlight.Api/Configuration/ApiOptions.cs
Normal file
6
Moonlight.Api/Configuration/ApiOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class ApiOptions
|
||||||
|
{
|
||||||
|
public int LookupCacheMinutes { get; set; } = 3;
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ public class UserActionsController : Controller
|
|||||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
||||||
await UsersRepository.UpdateAsync(user);
|
await UsersRepository.UpdateAsync(user);
|
||||||
|
|
||||||
Cache.Remove(string.Format(UserAuthService.ValidationCacheKeyPattern, id));
|
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, id));
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
||||||
|
|
||||||
|
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
||||||
|
{
|
||||||
|
public TimeSpan LookupCacheTime { get; set; }
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ public class UserAuthService
|
|||||||
private const string UserIdClaim = "UserId";
|
private const string UserIdClaim = "UserId";
|
||||||
private const string IssuedAtClaim = "IssuedAt";
|
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(
|
public UserAuthService(
|
||||||
DatabaseRepository<User> userRepository,
|
DatabaseRepository<User> userRepository,
|
||||||
@@ -88,8 +88,8 @@ public class UserAuthService
|
|||||||
if (!int.TryParse(userIdString, out var userId))
|
if (!int.TryParse(userIdString, out var userId))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var cacheKey = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{userId}";
|
var cacheKey = string.Format(CacheKeyPattern, userId);
|
||||||
|
|
||||||
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
|
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
|
||||||
{
|
{
|
||||||
user = await UserRepository
|
user = await UserRepository
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Moonlight.Api.Configuration;
|
using Moonlight.Api.Configuration;
|
||||||
using Moonlight.Api.Implementations;
|
using Moonlight.Api.Implementations;
|
||||||
|
using Moonlight.Api.Implementations.ApiKeyScheme;
|
||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
@@ -18,9 +20,17 @@ public partial class Startup
|
|||||||
var oidcOptions = new OidcOptions();
|
var oidcOptions = new OidcOptions();
|
||||||
builder.Configuration.GetSection("Moonlight:Oidc").Bind(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.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 =>
|
.AddCookie("Session", null, options =>
|
||||||
{
|
{
|
||||||
options.Events.OnSigningIn += async context =>
|
options.Events.OnSigningIn += async context =>
|
||||||
@@ -80,8 +90,14 @@ public partial class Startup
|
|||||||
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
|
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
|
||||||
|
|
||||||
options.GetClaimsFromUserInfoEndpoint = true;
|
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<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ public class CreateApiKeyDto
|
|||||||
{
|
{
|
||||||
[MaxLength(30)]
|
[MaxLength(30)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
[MaxLength(300)]
|
[MaxLength(300)] public string Description { get; set; } = "";
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
public string[] Permissions { get; set; }
|
public string[] Permissions { get; set; }
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,8 @@ public class UpdateApiKeyDto
|
|||||||
{
|
{
|
||||||
[MaxLength(30)]
|
[MaxLength(30)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
[MaxLength(300)]
|
[MaxLength(300)] public string Description { get; set; } = "";
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
public string[] Permissions { get; set; }
|
public string[] Permissions { get; set; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user