Bifrost/src/Bifrost.Web/Features/Oidc/OidcServiceCollectionExtensions.cs

84 lines
4 KiB
C#

using Bifrost.Web.Configuration;
using Bifrost.Web.Data;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Bifrost.Web.Features.Oidc;
public static class OidcServiceCollectionExtensions
{
public const string AuthorizationPath = "/connect/authorize";
public const string TokenPath = "/connect/token";
public const string UserInfoPath = "/connect/userinfo";
/// <summary>How long a parked authorize request (OpenIddict's request token) can wait for the user to sign in.</summary>
private static readonly TimeSpan ParkedRequestLifetime = TimeSpan.FromMinutes(10);
/// <summary>
/// Registers Bifrost as an OIDC provider whose issuer is <see cref="BifrostHostOptions.PublicBaseUrl"/>: authorization-code flow with
/// PKCE, ES256 id_tokens, scopes openid and profile, one configured client. Keys come from files, never from
/// generated development keys. The authorize request is parked by OpenIddict's own request caching, so the
/// sign-in return path carries only a short request_uri.
/// </summary>
public static IServiceCollection AddBifrostOidc(this IServiceCollection services, OidcIssuerOptions options)
{
var material = OidcKeyMaterial.Load(options);
services.AddSingleton(material);
services.AddSingleton(options);
services.AddOpenIddict()
.AddCore(core => core.UseEntityFrameworkCore().UseDbContext<BifrostDbContext>())
.AddServer(server =>
{
server.SetAuthorizationEndpointUris(AuthorizationPath)
.SetTokenEndpointUris(TokenPath)
.SetUserInfoEndpointUris(UserInfoPath);
server.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange();
server.RegisterScopes(Scopes.OpenId, Scopes.Profile);
server.RegisterClaims(Claims.Subject, Claims.PreferredUsername, Claims.Name);
server.EnableAuthorizationRequestCaching();
server.Configure(opts =>
{
opts.ClientAuthenticationMethods.Clear();
opts.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
opts.RequestTokenLifetime = ParkedRequestLifetime;
});
server.AddSigningKey(material.SigningKey);
server.AddEncryptionKey(material.EncryptionKey);
server.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserInfoEndpointPassthrough();
});
// The issuer is the public host, read from the bound options so it has one source.
services.AddOptions<OpenIddictServerOptions>().Configure<IOptions<BifrostHostOptions>>((server, host) =>
{
if (!Uri.TryCreate(host.Value.PublicBaseUrl, UriKind.Absolute, out var issuer))
{
throw new OidcConfigurationException("Host:PublicBaseUrl is required when Bifrost:Oidc is configured.");
}
server.Issuer = issuer;
});
services.AddSingleton<OidcRateLimit>();
services.AddSingleton<OidcPruner>();
services.AddHostedService(provider => provider.GetRequiredService<OidcPruner>());
services.AddHostedService<OidcClientSeeder>();
if (!string.IsNullOrWhiteSpace(options.Clients.Mas.BackchannelLogoutUri))
{
// MAS answers the logout POST itself: a redirect is a non-delivery, never followed to some other 2xx.
services.AddHttpClient(OidcLogoutReconciler.HttpClientName, client => client.Timeout = OidcLogoutReconciler.RequestTimeout)
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false });
services.AddHostedService<OidcLogoutReconciler>();
}
return services;
}
}