IdentityServiceExtensions.cs
1 using Fido2NetLib; 2 using GUNRPG.Application.Identity; 3 using GUNRPG.Infrastructure.Identity; 4 using Microsoft.AspNetCore.Identity; 5 using Microsoft.Extensions.DependencyInjection; 6 using Microsoft.Extensions.Options; 7 8 namespace GUNRPG.Infrastructure; 9 10 /// <summary> 11 /// Extension methods for registering the GunRPG identity system (ASP.NET Identity + WebAuthn + JWT). 12 /// </summary> 13 public static class IdentityServiceExtensions 14 { 15 /// <summary> 16 /// Registers all identity infrastructure services: 17 /// ASP.NET Identity backed by LiteDB, JWT token issuance (Ed25519), WebAuthn (Fido2NetLib), 18 /// and Device Code Flow for console clients. 19 /// </summary> 20 /// <param name="services">The service collection.</param> 21 /// <param name="verificationUri"> 22 /// The publicly reachable URI users visit to complete device code authorization. 23 /// Must be HTTPS for WebAuthn. Example: "https://yourdomain/auth/device/verify" 24 /// </param> 25 public static IServiceCollection AddGunRpgIdentity( 26 this IServiceCollection services, 27 string verificationUri) 28 { 29 // ── ASP.NET Identity ───────────────────────────────────────────────── 30 services 31 .AddIdentityCore<ApplicationUser>(opts => 32 { 33 // WebAuthn users have no password; disable password requirements. 34 opts.Password.RequireDigit = false; 35 opts.Password.RequireLowercase = false; 36 opts.Password.RequireUppercase = false; 37 opts.Password.RequireNonAlphanumeric = false; 38 opts.Password.RequiredLength = 0; 39 }) 40 .AddUserStore<LiteDbUserStore>(); 41 42 // ── WebAuthn storage ───────────────────────────────────────────────── 43 services.AddSingleton<LiteDbWebAuthnStore>(); 44 45 // ── Fido2NetLib ────────────────────────────────────────────────────── 46 services.AddSingleton<IValidateOptions<Fido2Configuration>, Fido2ConfigurationValidator>(); 47 services.AddOptions<Fido2Configuration>().ValidateOnStart(); 48 services.AddSingleton<IFido2>(sp => 49 { 50 var cfg = sp.GetRequiredService<IOptions<Fido2Configuration>>().Value; 51 return new Fido2(cfg); 52 }); 53 54 // ── JWT token service (Ed25519) ────────────────────────────────────── 55 services.AddSingleton<JwtTokenService>(); 56 services.AddSingleton<ITokenService>(sp => sp.GetRequiredService<JwtTokenService>()); 57 services.AddSingleton<IPublicKeyProvider>(sp => sp.GetRequiredService<JwtTokenService>()); 58 59 // ── WebAuthn service ───────────────────────────────────────────────── 60 services.AddScoped<IWebAuthnService, WebAuthnService>(); 61 62 // ── Device Code service ────────────────────────────────────────────── 63 services.AddScoped<IDeviceCodeService>(sp => 64 new DeviceCodeService( 65 sp.GetRequiredService<LiteDB.ILiteDatabase>(), 66 sp.GetRequiredService<ITokenService>(), 67 sp.GetRequiredService<UserManager<ApplicationUser>>(), 68 verificationUri)); 69 70 return services; 71 } 72 73 private sealed class Fido2ConfigurationValidator : IValidateOptions<Fido2Configuration> 74 { 75 public ValidateOptionsResult Validate(string? name, Fido2Configuration options) 76 { 77 var errors = GetWebAuthnOriginErrors(options.Origins).ToArray(); 78 return errors.Length == 0 79 ? ValidateOptionsResult.Success 80 : ValidateOptionsResult.Fail(errors); 81 } 82 83 private static IEnumerable<string> GetWebAuthnOriginErrors(IReadOnlySet<string> origins) 84 { 85 foreach (var origin in origins) 86 { 87 if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri)) 88 { 89 yield return $"WebAuthn origin '{origin}' is not a valid absolute URI."; 90 continue; 91 } 92 93 var isLocalhost = uri.IsLoopback; 94 if (!isLocalhost && !uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) 95 yield return $"WebAuthn origin '{origin}' must use HTTPS. HTTP origins are only permitted for localhost (development)."; 96 } 97 } 98 } 99 }