diff --git a/Wollmilchsau/Components/Layout/MainLayout.razor b/Wollmilchsau/Components/Layout/MainLayout.razor index 3f00865..adae076 100644 --- a/Wollmilchsau/Components/Layout/MainLayout.razor +++ b/Wollmilchsau/Components/Layout/MainLayout.razor @@ -6,6 +6,28 @@ + + + + + @{ + var user = authenticationState.User; + var displayName = string.Join(" ", new[] + { + user.FindFirst("given_name")?.Value, + user.FindFirst("family_name")?.Value + }.Where(value => !string.IsNullOrWhiteSpace(value))); + var classroom = user.FindFirst("classroom")?.Value + ?? (user.IsInRole("ROLE_TEACHER") ? "Lehrer" : null); + } + @displayName@(classroom is null ? string.Empty : $" ({classroom})") +
+ + Logout + +
+
+
@Body diff --git a/Wollmilchsau/Components/_Imports.razor b/Wollmilchsau/Components/_Imports.razor index d155b97..f6a600c 100644 --- a/Wollmilchsau/Components/_Imports.razor +++ b/Wollmilchsau/Components/_Imports.razor @@ -1,5 +1,6 @@ @using System.Net.Http @using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @@ -9,4 +10,4 @@ @using Wollmilchsau @using Wollmilchsau.Components @using Wollmilchsau.Components.Layout -@using MudBlazor \ No newline at end of file +@using MudBlazor diff --git a/Wollmilchsau/Program.cs b/Wollmilchsau/Program.cs index e0c7c29..3f0f370 100644 --- a/Wollmilchsau/Program.cs +++ b/Wollmilchsau/Program.cs @@ -1,14 +1,76 @@ using Fluxor; using JasperFx; using Marten; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using MudBlazor.Services; using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Json; using Wollmilchsau.Components; using Wollmilchsau.Exceptions; using Wollmilchsau.Services.Ldap; var builder = WebApplication.CreateBuilder(args); +var oidcConfiguration = builder.Configuration.GetRequiredSection("OIDC"); +var oidcScopes = oidcConfiguration.GetSection("Scopes").Get() + ?? throw new ConfigurationException("OIDC:Scopes"); + +builder.Services.AddAuthentication(options => +{ + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; +}) +.AddCookie() +.AddOpenIdConnect(options => +{ + options.Authority = oidcConfiguration["Authority"] ?? throw new ConfigurationException("OIDC:Authority"); + options.ClientId = oidcConfiguration["ClientId"] ?? throw new ConfigurationException("OIDC:ClientId"); + options.ClientSecret = oidcConfiguration["ClientSecret"] ?? throw new ConfigurationException("OIDC:ClientSecret"); + options.ResponseType = oidcConfiguration["ResponseType"] ?? throw new ConfigurationException("OIDC:ResponseType"); + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; + options.MapInboundClaims = false; + options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable; + options.Scope.Clear(); + + foreach (var scope in oidcScopes) + { + options.Scope.Add(scope); + } + + options.Events.OnTokenValidated = context => + { + AddUserClaims( + context.Principal?.Identity as ClaimsIdentity, + context.Principal?.FindAll("groups").Select(claim => claim.Value).ToArray() ?? [], + context.Principal?.FindFirst("klasse")?.Value); + + return Task.CompletedTask; + }; + options.Events.OnUserInformationReceived = context => + { + var userInfo = context.User.RootElement; + var groups = userInfo.TryGetProperty("groups", out var groupsElement) && groupsElement.ValueKind == JsonValueKind.Array + ? groupsElement.EnumerateArray() + .Where(group => group.ValueKind == JsonValueKind.String) + .Select(group => group.GetString()) + .OfType() + : []; + var classroom = userInfo.TryGetProperty("klasse", out var classroomElement) && classroomElement.ValueKind == JsonValueKind.String + ? classroomElement.GetString() + : null; + + AddUserClaims(context.Principal?.Identity as ClaimsIdentity, groups, classroom); + + return Task.CompletedTask; + }; +}); +builder.Services.AddAuthorization(); +builder.Services.AddCascadingAuthenticationState(); + builder.Services.AddOptions() .BindConfiguration("LDAP") .ValidateDataAnnotations() @@ -70,10 +132,47 @@ if (!app.Environment.IsDevelopment()) app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + app.UseAntiforgery(); app.MapStaticAssets(); +app.MapPost("/logout", async (HttpContext context) => + { + var authenticationResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + var properties = authenticationResult.Properties ?? new AuthenticationProperties(); + properties.RedirectUri = "/"; + + return Results.SignOut( + properties, + [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]); + }) + .RequireAuthorization() + .WithMetadata(new Microsoft.AspNetCore.Antiforgery.RequireAntiforgeryTokenAttribute(true)); app.MapRazorComponents() - .AddInteractiveServerRenderMode(); + .AddInteractiveServerRenderMode() + .RequireAuthorization(); return await app.RunJasperFxCommands(args); + +static void AddUserClaims(ClaimsIdentity? identity, IEnumerable groups, string? classroom) +{ + if (identity is null) + { + return; + } + + foreach (var group in groups.Where(group => !string.IsNullOrWhiteSpace(group))) + { + if (!identity.HasClaim(ClaimTypes.Role, group)) + { + identity.AddClaim(new Claim(ClaimTypes.Role, group)); + } + } + + if (!string.IsNullOrWhiteSpace(classroom) && !identity.HasClaim("classroom", classroom)) + { + identity.AddClaim(new Claim("classroom", classroom)); + } +} diff --git a/Wollmilchsau/appsettings.Development.json b/Wollmilchsau/appsettings.Development.json index 510c3b6..52f985b 100644 --- a/Wollmilchsau/appsettings.Development.json +++ b/Wollmilchsau/appsettings.Development.json @@ -31,10 +31,10 @@ }, "OIDC": { "Authority": "https://id.bs18.de/realms/master", - "ClientId": "WollmilchsauDev", + "ClientId": "wollmilchsaudev", "ClientSecret": "WQlhc6lzngs0nkC1TQeRbw16qffHaeg4", "ResponseType": "code", - "Scopes": [ "openid", "profile" ] + "Scopes": [ "openid", "profile", "gruppen", "KlassenAttribut" ] }, "MSGraph": { "ClientId": "c938782f-2779-45e6-8430-21238e69aa9e",