Files
FictionArchive/FictionArchive.Service.Shared/Extensions/AuthenticationExtensions.cs
Claude 9c82d648cd fix: address authentication system issues
- Fix GraphQL authorization attributes to use string[] instead of string for roles
- Remove admin role requirement from ImportNovel endpoint
- Add comprehensive OIDC configuration validation with specific error messages
- Validate Authority, ClientId, and Audience are properly configured
- Ensure HTTPS requirement except for localhost development

Co-authored-by: conco <conco@users.noreply.local>
2025-11-27 16:20:09 +00:00

121 lines
4.9 KiB
C#

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
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,
ValidateAudience = oidcConfig.ValidateAudience,
ValidateLifetime = oidcConfig.ValidateLifetime,
ValidateIssuerSigningKey = oidcConfig.ValidateIssuerSigningKey,
ClockSkew = TimeSpan.FromMinutes(5)
};
});
return services;
}
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://");
options.Events = 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.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = oidcConfig.ValidateIssuer,
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("Admin", policy => policy.RequireRole("admin"))
.AddPolicy("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)}");
}
}
}