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:
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;
|
||||
await UsersRepository.UpdateAsync(user);
|
||||
|
||||
Cache.Remove(string.Format(UserAuthService.ValidationCacheKeyPattern, id));
|
||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, id));
|
||||
|
||||
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 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,7 +88,7 @@ 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))
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ public class CreateApiKeyDto
|
||||
[MaxLength(30)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(300)]
|
||||
public string Description { get; set; }
|
||||
[MaxLength(300)] public string Description { get; set; } = "";
|
||||
|
||||
public string[] Permissions { get; set; }
|
||||
}
|
||||
@@ -7,8 +7,7 @@ public class UpdateApiKeyDto
|
||||
[MaxLength(30)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(300)]
|
||||
public string Description { get; set; }
|
||||
[MaxLength(300)] public string Description { get; set; } = "";
|
||||
|
||||
public string[] Permissions { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user