/ GUNRPG.Infrastructure / Identity / IdentityServiceExtensions.cs
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  }