193 lines
6.1 KiB
C#
193 lines
6.1 KiB
C#
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<AppDbContext>();
|
|
})
|
|
.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<HandleTokenRequestContext>(builder =>
|
|
builder.UseScopedHandler<PasswordGrantHandler>());
|
|
|
|
options.AddEventHandler<HandleTokenRequestContext>(builder =>
|
|
builder.UseScopedHandler<ExternalGrantHandler>());
|
|
})
|
|
.AddValidation(options =>
|
|
{
|
|
options.UseLocalServer();
|
|
options.UseAspNetCore();
|
|
});
|
|
|
|
services.AddAuthentication(options =>
|
|
{
|
|
options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
|
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
|
options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
|
});
|
|
|
|
services.AddHostedService<OpenIddictSeeder>();
|
|
|
|
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<string>(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);
|
|
}
|
|
}
|