namespace MyOffice.Web.Auth; using System.Security.Cryptography; using DbContext; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using OpenIddict.Server; using OpenIddict.Validation.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; public static class OpenIddictServiceCollectionExtensions { public static IServiceCollection AddMyOfficeOpenIddict( this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment ) { services.AddOpenIddict() .AddCore(options => { options.UseEntityFrameworkCore() .UseDbContext(); }) .AddServer(options => { options.SetAuthorizationEndpointUris("/connect/authorize") .SetTokenEndpointUris("/connect/token") .SetUserInfoEndpointUris("/connect/userinfo") .SetEndSessionEndpointUris("/connect/logout"); options.AllowAuthorizationCodeFlow() .AllowPasswordFlow() .AllowRefreshTokenFlow() .AllowCustomFlow(OpenIddictAuthConstants.ExternalGrantType); options.RegisterScopes( Scopes.OpenId, Scopes.Email, Scopes.Profile, Scopes.Roles, Scopes.OfflineAccess, OpenIddictAuthConstants.ApiScope, OpenIddictAuthConstants.RolesScope); ConfigureCryptography(options, configuration, environment); var aspNetCoreBuilder = options.UseAspNetCore() .EnableAuthorizationEndpointPassthrough() .EnableUserInfoEndpointPassthrough() .EnableEndSessionEndpointPassthrough(); // Local HTTP only. In production nginx terminates TLS and // ForwardedHeaders restores https:// for OpenIddict. // Docker demo also serves plain HTTP (OpenIddict:AllowHttp / Environment=Docker). if (environment.IsDevelopment() || IsDockerEnvironment(environment) || configuration.GetValue("OpenIddict:AllowHttp", false)) { aspNetCoreBuilder.DisableTransportSecurityRequirement(); } // Password and external grants need custom handlers (separate registrations — // a second UseScopedHandler() on the same builder replaces the first). options.AddEventHandler(builder => builder.UseScopedHandler()); options.AddEventHandler(builder => builder.UseScopedHandler()); }) .AddValidation(options => { options.UseLocalServer(); options.UseAspNetCore(); }); services.AddAuthentication(options => { options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; }); services.AddHostedService(); return services; } private static void ConfigureCryptography( OpenIddictServerBuilder options, IConfiguration configuration, IHostEnvironment environment ) { // Dev: certs in the user profile. Docker/demo: ephemeral keys (no cert store in containers). if (string.IsNullOrWhiteSpace(configuration["OpenIddict:SigningKeyPath"]) && (environment.IsDevelopment() || IsDockerEnvironment(environment) || configuration.GetValue("OpenIddict:UseEphemeralKeys", false))) { if (IsDockerEnvironment(environment) || configuration.GetValue("OpenIddict:UseEphemeralKeys", false)) { options.AddEphemeralEncryptionKey() .AddEphemeralSigningKey(); } else { options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); } return; } var contentRoot = configuration.GetValue(WebHostDefaults.ContentRootKey) ?? throw new InvalidOperationException("Content root path is not configured."); var signingPath = ResolveKeyPath( configuration["OpenIddict:SigningKeyPath"], contentRoot, "keys/signing.pem"); var encryptionPath = ResolveKeyPath( configuration["OpenIddict:EncryptionKeyPath"], contentRoot, "keys/encryption.pem"); if (!File.Exists(signingPath)) { throw new InvalidOperationException( $"OpenIddict signing key not found at '{signingPath}'. " + "Set OpenIddict:SigningKeyPath or place keys/signing.pem under the content root."); } options.AddSigningKey(LoadRsaSecurityKey(signingPath)); if (File.Exists(encryptionPath) && !string.Equals(encryptionPath, signingPath, StringComparison.OrdinalIgnoreCase)) { options.AddEncryptionKey(LoadRsaSecurityKey(encryptionPath)); } else { // Prefer a dedicated encryption key. Fallback keeps older single-key setups working. options.AddEncryptionKey(LoadRsaSecurityKey(signingPath)); } } private static bool IsDockerEnvironment(IHostEnvironment environment) => string.Equals(environment.EnvironmentName, "Docker", StringComparison.OrdinalIgnoreCase); private static string ResolveKeyPath(string? configuredPath, string contentRoot, string defaultRelative) { if (!string.IsNullOrWhiteSpace(configuredPath)) { return Path.IsPathRooted(configuredPath) ? configuredPath : Path.GetFullPath(Path.Combine(contentRoot, configuredPath)); } return Path.GetFullPath(Path.Combine(contentRoot, defaultRelative)); } private static RsaSecurityKey LoadRsaSecurityKey(string path) { var privateKey = File.ReadAllText(path) .Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) .Replace("-----END RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) .Replace("-----BEGIN PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) .Replace("-----END PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) .Replace("\r", string.Empty, StringComparison.Ordinal) .Replace("\n", string.Empty, StringComparison.Ordinal); var rsa = RSA.Create(); var keyBytes = Convert.FromBase64String(privateKey); try { rsa.ImportRSAPrivateKey(keyBytes, out _); } catch (CryptographicException) { rsa.ImportPkcs8PrivateKey(keyBytes, out _); } return new RsaSecurityKey(rsa); } }