168 lines
7.1 KiB
C#
168 lines
7.1 KiB
C#
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using FictionArchive.Service.Shared.Constants;
|
|
using FictionArchive.Service.Shared.Models.Authentication;
|
|
using System.Linq;
|
|
|
|
namespace FictionArchive.Service.Shared.Extensions;
|
|
|
|
public static class AuthenticationExtensions
|
|
{
|
|
public static IServiceCollection AddOidcAuthentication(this IServiceCollection services, IConfiguration configuration)
|
|
{
|
|
var oidcConfig = configuration.GetSection("OIDC").Get<OidcConfiguration>();
|
|
|
|
if (oidcConfig == null)
|
|
{
|
|
throw new InvalidOperationException("OIDC configuration is required but not found in app settings");
|
|
}
|
|
|
|
ValidateOidcConfiguration(oidcConfig);
|
|
|
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.Authority = oidcConfig.Authority;
|
|
options.Audience = oidcConfig.Audience;
|
|
options.RequireHttpsMetadata = !string.IsNullOrEmpty(oidcConfig.Authority) && oidcConfig.Authority.StartsWith("https://");
|
|
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = oidcConfig.ValidateIssuer,
|
|
ValidIssuer = oidcConfig.ValidIssuer,
|
|
ValidateAudience = oidcConfig.ValidateAudience,
|
|
ValidateLifetime = oidcConfig.ValidateLifetime,
|
|
ValidateIssuerSigningKey = oidcConfig.ValidateIssuerSigningKey,
|
|
ClockSkew = TimeSpan.FromMinutes(5)
|
|
};
|
|
|
|
options.Events = CreateLoggingJwtBearerEvents();
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
private static JwtBearerEvents CreateLoggingJwtBearerEvents(JwtBearerEvents? existingEvents = null)
|
|
{
|
|
return new JwtBearerEvents
|
|
{
|
|
OnMessageReceived = existingEvents?.OnMessageReceived ?? (_ => Task.CompletedTask),
|
|
OnAuthenticationFailed = context =>
|
|
{
|
|
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
|
|
.CreateLogger("JwtBearerAuthentication");
|
|
|
|
logger.LogWarning(context.Exception, "JWT authentication failed: {Message}", context.Exception.Message);
|
|
|
|
return existingEvents?.OnAuthenticationFailed?.Invoke(context) ?? Task.CompletedTask;
|
|
},
|
|
OnChallenge = context =>
|
|
{
|
|
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
|
|
.CreateLogger("JwtBearerAuthentication");
|
|
|
|
logger.LogDebug(
|
|
"JWT challenge issued. Error: {Error}, ErrorDescription: {ErrorDescription}",
|
|
context.Error,
|
|
context.ErrorDescription);
|
|
|
|
return existingEvents?.OnChallenge?.Invoke(context) ?? Task.CompletedTask;
|
|
},
|
|
OnTokenValidated = context =>
|
|
{
|
|
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
|
|
.CreateLogger("JwtBearerAuthentication");
|
|
|
|
logger.LogDebug(
|
|
"JWT token validated for subject: {Subject}",
|
|
context.Principal?.FindFirst("sub")?.Value ?? "unknown");
|
|
|
|
return existingEvents?.OnTokenValidated?.Invoke(context) ?? Task.CompletedTask;
|
|
}
|
|
};
|
|
}
|
|
|
|
public static IServiceCollection AddOidcCookieAuthentication(this IServiceCollection services, IConfiguration configuration, string cookieName = "fa_session")
|
|
{
|
|
var oidcConfig = configuration.GetSection("OIDC").Get<OidcConfiguration>();
|
|
|
|
if (oidcConfig == null)
|
|
{
|
|
throw new InvalidOperationException("OIDC configuration is required but not found in app settings");
|
|
}
|
|
|
|
ValidateOidcConfiguration(oidcConfig);
|
|
|
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.Authority = oidcConfig.Authority;
|
|
options.Audience = oidcConfig.Audience;
|
|
options.RequireHttpsMetadata = !string.IsNullOrEmpty(oidcConfig.Authority) && oidcConfig.Authority.StartsWith("https://");
|
|
|
|
var cookieEvents = new JwtBearerEvents
|
|
{
|
|
OnMessageReceived = context =>
|
|
{
|
|
// Try to get token from cookie first, then from Authorization header
|
|
if (context.Request.Cookies.ContainsKey(cookieName))
|
|
{
|
|
context.Token = context.Request.Cookies[cookieName];
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
options.Events = CreateLoggingJwtBearerEvents(cookieEvents);
|
|
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = oidcConfig.ValidateIssuer,
|
|
ValidIssuer = oidcConfig.ValidIssuer,
|
|
ValidateAudience = oidcConfig.ValidateAudience,
|
|
ValidateLifetime = oidcConfig.ValidateLifetime,
|
|
ValidateIssuerSigningKey = oidcConfig.ValidateIssuerSigningKey,
|
|
ClockSkew = TimeSpan.FromMinutes(5)
|
|
};
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
public static IServiceCollection AddFictionArchiveAuthorization(this IServiceCollection services)
|
|
{
|
|
services.AddAuthorizationBuilder()
|
|
.AddPolicy(AuthorizationConstants.Policies.Admin, policy => policy.RequireRole(AuthorizationConstants.Roles.Admin))
|
|
.AddPolicy(AuthorizationConstants.Policies.User, policy => policy.RequireAuthenticatedUser());
|
|
|
|
return services;
|
|
}
|
|
|
|
private static void ValidateOidcConfiguration(OidcConfiguration config)
|
|
{
|
|
var errors = new List<string>();
|
|
|
|
if (string.IsNullOrWhiteSpace(config.Authority))
|
|
errors.Add("OIDC Authority is required");
|
|
|
|
if (string.IsNullOrWhiteSpace(config.ClientId))
|
|
errors.Add("OIDC ClientId is required");
|
|
|
|
if (string.IsNullOrWhiteSpace(config.Audience))
|
|
errors.Add("OIDC Audience is required");
|
|
|
|
if (!Uri.TryCreate(config.Authority, UriKind.Absolute, out var authorityUri))
|
|
errors.Add($"OIDC Authority '{config.Authority}' is not a valid URI");
|
|
else if (!authorityUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
|
!authorityUri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
|
errors.Add("OIDC Authority must use HTTPS unless running on localhost");
|
|
|
|
if (errors.Any())
|
|
{
|
|
throw new InvalidOperationException($"OIDC configuration validation failed:{Environment.NewLine}{string.Join(Environment.NewLine, errors)}");
|
|
}
|
|
}
|
|
} |