Hoi
Ik heb een probleem waar ik met Google/SO/ChatGPT/Claude/Tutorials niet uit raak. Onderstaande toepassing heb ik in het verleden geschreven voor .NET Framework 4.8, maar is nu aan een rewrite toe. Ik ben nieuw in het ASP.NET Core-verhaal, dus maak ik rookie mistakes, zeg dat gerust.
Wie kan me in de juiste richting zetten?
Alvast bedankt!
Ik heb een class Persoon met 2 subclasses Leerling en Leerkracht. De class ApplicationUser link ik 1 op 1 met een persoon.
De gebruikers worden opgeslagen in een SQL-Server DB: na aanmelding moet gekeken worden of de gebruiker al in de db zit. Indien wel, worden daaruit de rollen ingeladen. Indien nog niet, worden de rollen automatisch bepaald op basis van de Entra-claims en moeten die zo worden opgeslagen in de DB.
Het probleem treedt op wanneer ik de custom middleware (die ik geschreven heb met behulp van Claude) wil gebruik. Ik kan mijn gebruiker nog wel aanmelden bij de Microsoft School Account, maar in mijn toepassing blijft HttpContext.User.Identity.IsAuthenticated false.
UserService
SportdagAuthenticationMiddleware
Ik heb een probleem waar ik met Google/SO/ChatGPT/Claude/Tutorials niet uit raak. Onderstaande toepassing heb ik in het verleden geschreven voor .NET Framework 4.8, maar is nu aan een rewrite toe. Ik ben nieuw in het ASP.NET Core-verhaal, dus maak ik rookie mistakes, zeg dat gerust.
Wie kan me in de juiste richting zetten?
Alvast bedankt!
Context
In een school kunnen gebruikers zich via een webtoepassing registreren voor sportdagactiviteiten: leerlingen kunnen hun keuze (=een keuze uit een lijst die afhankelijk is van het leerjaar) maken voor een activiteit, leerkrachten kunnen kiezen welke activiteit ze begeleiden. De leerlingen schrijven zich in "golven" in: gedurende 1 week kunnen zij hun keuze maken en eventueel nog wijzigen (binnen diezelfde week). Het beginmoment (typisch om 18u) is telkens zeer druk: dan zijn ze met enkele honderden tegelijk aangemeld om toch maar een plaatsje te bemachtigen voor hun favoriete activiteit (uiteraard is het aantal plaatsen per activiteit beperkt).Technisch
Een ASP.NET 9.0 MVC Web Application waar de gebruikers zich aanmelden met een Microsoft Work or School Account. De rollen "Leerling" en "Leerling" moeten automatisch toegekend worden op basis van de claims uit Entra. Voor een leerling kan ook de klas daaruit afgeleid worden. Daarnaast kunnen in de toepassing zelf de rollen ook beheerd worden: "organisatoren" en "beheerders" kunnen toegewezen worden aan bepaalde gebruikers.Ik heb een class Persoon met 2 subclasses Leerling en Leerkracht. De class ApplicationUser link ik 1 op 1 met een persoon.
De gebruikers worden opgeslagen in een SQL-Server DB: na aanmelding moet gekeken worden of de gebruiker al in de db zit. Indien wel, worden daaruit de rollen ingeladen. Indien nog niet, worden de rollen automatisch bepaald op basis van de Entra-claims en moeten die zo worden opgeslagen in de DB.
Probleem
Ik slaag er niet om het geheel werkend te krijgen. Het aanmelden op zich met een Microsoft School Account werkt wel (uiteraard), ook het inlezen van de claims uit Entra en het bepalen van de Leerkrachten- en Leerlingen-rollen en deze met een claim toevoegen aan de identity. Mijn appsettings staan dus correct.Het probleem treedt op wanneer ik de custom middleware (die ik geschreven heb met behulp van Claude) wil gebruik. Ik kan mijn gebruiker nog wel aanmelden bij de Microsoft School Account, maar in mijn toepassing blijft HttpContext.User.Identity.IsAuthenticated false.
Relevante code
Program.cs (ik weet dat het een hele lap is, maar vrees dat dit moeilijk anders kanC#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.Build.Framework; using Microsoft.EntityFrameworkCore; using Microsoft.Identity.Web; using Microsoft.Identity.Web.UI; using Sportdag_Claude.Data; using Sportdag_Claude.Infrastructure; using Sportdag_Claude.Models; using Sportdag_Claude.Services; var builder = WebApplication.CreateBuilder(args); var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' '); // Add services to the container. builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph")) .AddInMemoryTokenCaches(); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages() .AddMicrosoftIdentityUI(); builder.Services.AddDbContext<SportdagContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("SportdagContext") ?? throw new InvalidOperationException("Connection string 'SportdagContext' not found."))); // Na AddDbContext, maar voor de andere services builder.Services.AddIdentity<ApplicationUser, Rol>(options => { options.SignIn.RequireConfirmedAccount = false; // Configure other options as needed }) .AddEntityFrameworkStores<SportdagContext>() .AddDefaultTokenProviders(); builder.Services.Configure<DefinedRoles>(builder.Configuration.GetSection("Rollen")); builder.Services.AddSingleton<DefinedRoleService>(); builder.Services.AddScoped<UserService>(); builder.Services.AddScoped<RoleMappingService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthentication(); // Add after UseAuthentication but before UseAuthorization app.UseMiddleware<SportdagAuthenticationMiddleware>(); app.UseAuthorization(); app.MapStaticAssets(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}") .WithStaticAssets(); app.MapRazorPages() .WithStaticAssets(); app.Run(); |
UserService
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| using Microsoft.AspNetCore.Identity; using Sportdag_Claude.Data; using Sportdag_Claude.Models; using System.Security.Claims; namespace Sportdag_Claude.Services { public interface IUserService { Task<ApplicationUser> GetCurrentUserAsync(ClaimsPrincipal claimsPrincipal); Task<bool> IsTeacherAsync(ClaimsPrincipal claimsPrincipal); Task<bool> IsStudentAsync(ClaimsPrincipal claimsPrincipal); Task<int> GetStudentGradeAsync(ClaimsPrincipal claimsPrincipal); } public class UserService : IUserService { private readonly UserManager<ApplicationUser> _userManager; private readonly SportdagContext _context; private readonly ILogger<UserService> _logger; public UserService(UserManager<ApplicationUser> userManager, SportdagContext context, ILogger<UserService> logger) { _userManager = userManager; _context = context; _logger = logger; } public async Task<ApplicationUser> GetCurrentUserAsync(ClaimsPrincipal principal) { // Get Azure AD object ID (unique identifier) var objectId = principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier"); // Check if user exists var user = await _userManager.FindByIdAsync(objectId); if (user != null) return user; // Create new user and link to Persoon user = new ApplicationUser { Id = objectId, UserName = principal.FindFirstValue("preferred_username"), Email = principal.FindFirstValue("email"), DisplayName = principal.FindFirstValue("name") }; // Create corresponding Persoon //TODO: Implement CreatePersoonFromClaims //var persoon = await CreatePersoonFromClaims(principal); //user.PersoonId = persoon.Id; await _userManager.CreateAsync(user); return user; } public async Task<int> GetStudentGradeAsync(ClaimsPrincipal claimsPrincipal) { var user = await GetCurrentUserAsync(claimsPrincipal); if ((user?.Persoon is Leerling leerling)) { //TODO: Leerjaar implementeren en ophalen return -1; } throw new InvalidOperationException("User is not a student"); } public async Task<bool> IsStudentAsync(ClaimsPrincipal claimsPrincipal) { var user = await GetCurrentUserAsync(claimsPrincipal); return user?.Persoon is Leerling; } public async Task<bool> IsTeacherAsync(ClaimsPrincipal claimsPrincipal) { var user = await GetCurrentUserAsync(claimsPrincipal); return user?.Persoon is Leerkracht; } } } |
SportdagAuthenticationMiddleware
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| using Sportdag_Claude.Models; using Sportdag_Claude.Services; using System.Data; using System.Security.Claims; namespace Sportdag_Claude.Infrastructure { public class SportdagAuthenticationMiddleware { private readonly RequestDelegate _next; private readonly ILogger<SportdagAuthenticationMiddleware> _logger; private readonly DefinedRoleService _definedRoleService; private readonly IServiceScopeFactory _scopeFactory; // nodig omdat de userService scoped (en geen singleton) is public SportdagAuthenticationMiddleware(RequestDelegate next, ILogger<SportdagAuthenticationMiddleware> logger, DefinedRoleService definedRoleService, IServiceScopeFactory scopeFactory) { _next = next; _logger = logger; _definedRoleService = definedRoleService; _scopeFactory = scopeFactory; } public async Task InvokeAsync(HttpContext context) { if (context.User.Identity?.IsAuthenticated==true) { try { using var scope = _scopeFactory.CreateScope(); var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); var roleMappingService = scope.ServiceProvider.GetRequiredService<RoleMappingService>(); var user = await userService.GetCurrentUserAsync(context.User); // If user is new or roles need refresh, get roles from Azure AD var objectId = context.User.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier"); var roles = await roleMappingService.GetUserRolesAsync(objectId); // Enrich identity with our application roles // Add claims based on Persoon type (Leerling/Leerkracht) // Create new identity with existing claims var identity = new ClaimsIdentity(context.User.Identity); foreach(var role in roles) { if(!identity.HasClaim(ClaimTypes.Role, role)) identity.AddClaim(new Claim(ClaimTypes.Role, role)); } // Add role claims based on Persoon type if (user.Persoon!=null) { string roleKey = user.Persoon is Leerling ? "LL" : user.Persoon is Leerkracht ? "LK" : ""; string? rolename = _definedRoleService.GetRolnaam(roleKey); if (!string.IsNullOrEmpty(rolename) && !identity.HasClaim(ClaimTypes.Role, rolename)) identity.AddClaim(new Claim(ClaimTypes.Role, rolename)); // Add special role claims (Admin, Organisator) foreach (var rol in user.Persoon.Rollen) { identity.AddClaim(new Claim(ClaimTypes.Role, rol.Naam)); } } // Replace the current principal context.User = new ClaimsPrincipal(identity); } catch (Exception ex) { _logger.LogError(ex, "ERROR - SportdagAuthenticationMiddleware: Rollen toevoegen aan identity mislukt."); } } await _next(context); } } } |