Bifrost/src/Bifrost.Web/Program.cs
2026-04-21 20:08:49 +01:00

152 lines
5.5 KiB
C#

using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using Bifrost.Web.Configuration;
using Bifrost.Web.Data;
using Bifrost.Web.Features.Membership;
using Bifrost.Web.Features.Shared;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<GitHubOAuthOptions>()
.BindConfiguration(GitHubOAuthOptions.SectionName);
builder.Services
.AddOptions<BootstrapOptions>()
.BindConfiguration(BootstrapOptions.SectionName);
builder.Services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/Membership/Status");
options.Conventions.AuthorizeFolder("/App", ActiveMemberRequirement.PolicyName);
});
builder.Services.AddDbContext<BifrostDbContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("Bifrost")
?? "Host=127.0.0.1;Database=bifrost;Username=postgres;Password=postgres";
options.UseNpgsql(connectionString);
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped<ICurrentBifrostActorAccessor, CurrentBifrostActorAccessor>();
builder.Services.AddScoped<AuditTrailService>();
builder.Services.AddScoped<DashboardSnapshotService>();
builder.Services.AddScoped<MembershipSynchronizationService>();
builder.Services.AddScoped<IAuthorizationHandler, ActiveMemberAuthorizationHandler>();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/auth/sign-in";
options.AccessDeniedPath = "/Membership/Status";
options.SlidingExpiration = true;
})
.AddOAuth(GitHubAuthenticationDefaults.AuthenticationScheme, options =>
{
var gitHubOptions = builder.Configuration
.GetSection(GitHubOAuthOptions.SectionName)
.Get<GitHubOAuthOptions>() ?? new GitHubOAuthOptions();
options.ClientId = gitHubOptions.ClientId;
options.ClientSecret = gitHubOptions.ClientSecret;
options.CallbackPath = gitHubOptions.CallbackPath;
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user";
options.SaveTokens = true;
options.Scope.Add("read:user");
options.Scope.Add("user:email");
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
options.ClaimActions.MapJsonKey(BifrostClaimTypes.GitHubLogin, "login");
options.ClaimActions.MapJsonKey(BifrostClaimTypes.DisplayName, "name");
options.ClaimActions.MapJsonKey(BifrostClaimTypes.AvatarUrl, "avatar_url");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
using var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
request.Headers.UserAgent.ParseAdd("Bifrost");
using var response = await context.Backchannel.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
await using var payloadStream = await response.Content.ReadAsStreamAsync(context.HttpContext.RequestAborted);
using var payload = await JsonDocument.ParseAsync(payloadStream, cancellationToken: context.HttpContext.RequestAborted);
context.RunClaimActions(payload.RootElement);
}
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(ActiveMemberRequirement.PolicyName, policy =>
{
policy.RequireAuthenticatedUser();
policy.AddRequirements(new ActiveMemberRequirement());
});
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseMiddleware<GitHubMembershipSynchronizationMiddleware>();
app.UseAuthorization();
app.MapGet("/auth/sign-in", async (HttpContext httpContext, IOptions<GitHubOAuthOptions> gitHubOptions) =>
{
if (!gitHubOptions.Value.IsConfigured)
{
httpContext.Response.Redirect("/?auth=github-not-configured");
return;
}
var redirectUri = httpContext.Request.Query["returnUrl"].ToString();
if (string.IsNullOrWhiteSpace(redirectUri))
{
redirectUri = "/App";
}
await httpContext.ChallengeAsync(
GitHubAuthenticationDefaults.AuthenticationScheme,
new() { RedirectUri = redirectUri });
}).AllowAnonymous();
app.MapPost("/auth/sign-out", async (HttpContext httpContext) =>
{
await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
httpContext.Response.Redirect("/");
}).AllowAnonymous();
app.MapRazorPages();
app.Run();
public partial class Program;