diff --git a/Backend/Afra-App.slnx b/Backend/Afra-App.slnx index 68c181ed..ed69dad9 100644 --- a/Backend/Afra-App.slnx +++ b/Backend/Afra-App.slnx @@ -2,16 +2,15 @@ - + - - - - - - - - - - + + + + + + + + + diff --git a/Backend/Altafraner.AfraApp/Altafraner.AfraApp.csproj b/Backend/Altafraner.AfraApp/Altafraner.AfraApp.csproj index 833fc49e..7f7e1834 100644 --- a/Backend/Altafraner.AfraApp/Altafraner.AfraApp.csproj +++ b/Backend/Altafraner.AfraApp/Altafraner.AfraApp.csproj @@ -8,6 +8,7 @@ + @@ -42,7 +43,6 @@ - diff --git a/Backend/Altafraner.AfraApp/Backbone/Authorization/AfraAppClaimTypes.cs b/Backend/Altafraner.AfraApp/Backbone/Auth/AfraAppClaimTypes.cs similarity index 96% rename from Backend/Altafraner.AfraApp/Backbone/Authorization/AfraAppClaimTypes.cs rename to Backend/Altafraner.AfraApp/Backbone/Auth/AfraAppClaimTypes.cs index 20ffaaa6..c2e10888 100644 --- a/Backend/Altafraner.AfraApp/Backbone/Authorization/AfraAppClaimTypes.cs +++ b/Backend/Altafraner.AfraApp/Backbone/Auth/AfraAppClaimTypes.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using Altafraner.AfraApp.User.Domain.Models; -namespace Altafraner.AfraApp.Backbone.Authorization; +namespace Altafraner.AfraApp.Backbone.Auth; /// /// Specifies the claim types used in the for the Afra-App diff --git a/Backend/Altafraner.AfraApp/Backbone/Auth/AuthModule.cs b/Backend/Altafraner.AfraApp/Backbone/Auth/AuthModule.cs new file mode 100644 index 00000000..3d77fe15 --- /dev/null +++ b/Backend/Altafraner.AfraApp/Backbone/Auth/AuthModule.cs @@ -0,0 +1,180 @@ +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using Altafraner.AfraApp.Domain.Configuration; +using Altafraner.AfraApp.User.Domain.Models; +using Altafraner.AfraApp.User.Services; +using Altafraner.AfraApp.User.Services.LDAP; +using Altafraner.Backbone.Abstractions; +using Altafraner.Backbone.Defaults; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Altafraner.AfraApp.Backbone.Auth; + +/// +/// A module for handling simple authorization cases +/// +[DependsOn] +internal class AuthModule : IModule +{ + public void ConfigureServices(IServiceCollection services, IConfiguration config, IHostEnvironment env) + { + var cookieSection = config.GetSection("CookieAuthentication"); + services.AddOptions().Bind(cookieSection); + + var cookieSettings = cookieSection.Exists() + ? cookieSection.Get() ?? + throw new ValidationException("Cannot bind CookieAuthenticationSettings") + : new CookieAuthenticationSettings(); + + var oidcSection = config.GetSection("Oidc"); + services.AddOptions() + .Validate(OidcConfiguration.Validate) + .ValidateOnStart() + .Bind(oidcSection); + + + var oidcSettings = oidcSection.Exists() + ? oidcSection.Get() ?? + throw new ValidationException("Cannot bind OidcConfiguration") + : new OidcConfiguration(); + + var authBuilder = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + .AddCookie(options => + { + options.ExpireTimeSpan = cookieSettings.CookieTimeout; + options.Cookie.HttpOnly = true; + options.Cookie.SameSite = cookieSettings.SameSiteMode; + options.Cookie.SecurePolicy = cookieSettings.SecurePolicy; + options.SlidingExpiration = cookieSettings.SlidingExpiration; + }); + + if (oidcSettings.Enabled) + authBuilder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, + options => + { + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.ResponseType = OpenIdConnectResponseType.Code; + + options.Authority = oidcSettings.Authority; + options.ClientId = oidcSettings.ClientId; + options.ClientSecret = oidcSettings.ClientSecret; + + options.CallbackPath = new PathString("/api/oidc/signin"); + options.SignedOutCallbackPath = new PathString("/api/oidc/signout"); + + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.SaveTokens = false; + + options.Events = new OpenIdConnectEvents + { + OnTokenValidated = OidcOnTokenValidated, + OnAccessDenied = context => + { + var logger = context.HttpContext.RequestServices + .GetRequiredService>(); + logger.LogWarning("OIDC Access Denied"); + context.Response.Redirect("/oidc/access-denied"); + context.HandleResponse(); + return Task.CompletedTask; + }, + OnRemoteFailure = context => + { + var logger = context.HttpContext.RequestServices + .GetRequiredService>(); + logger.LogWarning("OIDC Unexpected Remote Error: {message}", context.Failure?.Message); + context.Response.Redirect("/oidc/remote-error"); + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + + services.AddAuthorizationBuilder() + .AddPolicy(AuthorizationPolicies.StudentOnly, + policy => policy.RequireClaim(AfraAppClaimTypes.Role, + nameof(Rolle.Oberstufe), nameof(Rolle.Mittelstufe))) + .AddPolicy(AuthorizationPolicies.MittelStufeStudentOnly, + policy => policy.RequireClaim(AfraAppClaimTypes.Role, + nameof(Rolle.Mittelstufe))) + .AddPolicy(AuthorizationPolicies.TutorOnly, + policy => policy.RequireClaim(AfraAppClaimTypes.Role, + nameof(Rolle.Tutor))) + .AddPolicy(AuthorizationPolicies.Otiumsverantwortlich, + policy => policy.RequireClaim(AfraAppClaimTypes.GlobalPermission, + nameof(GlobalPermission.Otiumsverantwortlich))) + .AddPolicy(AuthorizationPolicies.ProfundumsVerantwortlich, + policy => policy.RequireClaim(AfraAppClaimTypes.GlobalPermission, + nameof(GlobalPermission.Profundumsverantwortlich))) + .AddPolicy(AuthorizationPolicies.AdminOnly, + policy => policy.RequireClaim(AfraAppClaimTypes.GlobalPermission, + nameof(GlobalPermission.Admin))) + .AddPolicy(AuthorizationPolicies.TeacherOrAdmin, + policy => policy.RequireAssertion(context => + context.User.HasClaim(AfraAppClaimTypes.GlobalPermission, nameof(GlobalPermission.Admin)) + || context.User.HasClaim(AfraAppClaimTypes.Role, nameof(Rolle.Tutor)))); + } + + private static async Task OidcOnTokenValidated(TokenValidatedContext context) + { + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + var oidcSettings = context.HttpContext.RequestServices.GetRequiredService>().Value; + + var oidcUser = context.Principal; + var userId = oidcUser?.FindFirst(oidcSettings.IdClaim!)?.Value; + logger.LogWarning("UserId: {userId}", userId); + + if (userId is null) + { + logger.LogWarning("Received OIDC event without ID"); + context.Fail("The authentication provider did not provide a user ID"); + return; + } + + var userService = context.HttpContext.RequestServices.GetRequiredService(); + + var user = await userService.GetUserByLdapIdAsync(new Guid(userId)); + if (user is null) + { + var ldapService = context.HttpContext.RequestServices.GetRequiredService(); + await ldapService.SynchronizeAsync(); + user = await userService.GetUserByIdAsync(new Guid(userId)); + if (user is null) + { + context.Fail("User not staged for synchronization"); + return; + } + } + + var claims = UserSigninService.GenerateClaims(user); + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + context.Principal = principal; + context.Success(); + } + + public void RegisterMiddleware(WebApplication app) + { + app.UseAuthentication(); + app.UseAuthorization(); + } + + public void Configure(WebApplication app) + { + app.MapGet("/api/oidc/start", + () => TypedResults.Challenge(new AuthenticationProperties + { + RedirectUri = "/" + }, + [OpenIdConnectDefaults.AuthenticationScheme])); + } +} diff --git a/Backend/Altafraner.AfraApp/Backbone/Authorization/AuthorizationPolicies.cs b/Backend/Altafraner.AfraApp/Backbone/Auth/AuthorizationPolicies.cs similarity index 96% rename from Backend/Altafraner.AfraApp/Backbone/Authorization/AuthorizationPolicies.cs rename to Backend/Altafraner.AfraApp/Backbone/Auth/AuthorizationPolicies.cs index 54dbf258..0a42556e 100644 --- a/Backend/Altafraner.AfraApp/Backbone/Authorization/AuthorizationPolicies.cs +++ b/Backend/Altafraner.AfraApp/Backbone/Auth/AuthorizationPolicies.cs @@ -1,4 +1,4 @@ -namespace Altafraner.AfraApp.Backbone.Authorization; +namespace Altafraner.AfraApp.Backbone.Auth; /// /// A static class containing constants for authorization policies. diff --git a/Backend/Altafraner.Backbone.CookieAuthentication/Contracts/IAuthenticationLifetimeService.cs b/Backend/Altafraner.AfraApp/Backbone/Auth/IAuthenticationLifetimeService.cs similarity index 89% rename from Backend/Altafraner.Backbone.CookieAuthentication/Contracts/IAuthenticationLifetimeService.cs rename to Backend/Altafraner.AfraApp/Backbone/Auth/IAuthenticationLifetimeService.cs index 9e7d30d8..94d49f8b 100644 --- a/Backend/Altafraner.Backbone.CookieAuthentication/Contracts/IAuthenticationLifetimeService.cs +++ b/Backend/Altafraner.AfraApp/Backbone/Auth/IAuthenticationLifetimeService.cs @@ -1,6 +1,6 @@ using System.Security.Claims; -namespace Altafraner.Backbone.CookieAuthentication; +namespace Altafraner.AfraApp.Backbone.Auth; /// /// A service handling signing in and out users. diff --git a/Backend/Altafraner.Backbone.CookieAuthentication/Services/AuthenticationLifetimeService.cs b/Backend/Altafraner.AfraApp/Backbone/Auth/Services/AuthenticationLifetimeService.cs similarity index 91% rename from Backend/Altafraner.Backbone.CookieAuthentication/Services/AuthenticationLifetimeService.cs rename to Backend/Altafraner.AfraApp/Backbone/Auth/Services/AuthenticationLifetimeService.cs index 8bca85c3..ed882337 100644 --- a/Backend/Altafraner.Backbone.CookieAuthentication/Services/AuthenticationLifetimeService.cs +++ b/Backend/Altafraner.AfraApp/Backbone/Auth/Services/AuthenticationLifetimeService.cs @@ -1,8 +1,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -namespace Altafraner.Backbone.CookieAuthentication.Services; +namespace Altafraner.AfraApp.Backbone.Auth.Services; internal class AuthenticationLifetimeService : IAuthenticationLifetimeService { diff --git a/Backend/Altafraner.AfraApp/Backbone/Authorization/AuthorizationModule.cs b/Backend/Altafraner.AfraApp/Backbone/Authorization/AuthorizationModule.cs deleted file mode 100644 index 12181dcb..00000000 --- a/Backend/Altafraner.AfraApp/Backbone/Authorization/AuthorizationModule.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Altafraner.AfraApp.User.Domain.Models; -using Altafraner.Backbone.Abstractions; -using Altafraner.Backbone.CookieAuthentication; -using Altafraner.Backbone.Defaults; - -namespace Altafraner.AfraApp.Backbone.Authorization; - -/// -/// A module for handling simple authorization cases -/// -[DependsOn] -[DependsOn] -public class AuthorizationModule : IModule -{ - /// - public void ConfigureServices(IServiceCollection services, IConfiguration config, IHostEnvironment env) - { - services.AddAuthorizationBuilder() - .AddPolicy(AuthorizationPolicies.StudentOnly, - policy => policy.RequireClaim(AfraAppClaimTypes.Role, - nameof(Rolle.Oberstufe), nameof(Rolle.Mittelstufe))) - .AddPolicy(AuthorizationPolicies.MittelStufeStudentOnly, - policy => policy.RequireClaim(AfraAppClaimTypes.Role, - nameof(Rolle.Mittelstufe))) - .AddPolicy(AuthorizationPolicies.TutorOnly, - policy => policy.RequireClaim(AfraAppClaimTypes.Role, - nameof(Rolle.Tutor))) - .AddPolicy(AuthorizationPolicies.Otiumsverantwortlich, - policy => policy.RequireClaim(AfraAppClaimTypes.GlobalPermission, - nameof(GlobalPermission.Otiumsverantwortlich))) - .AddPolicy(AuthorizationPolicies.ProfundumsVerantwortlich, - policy => policy.RequireClaim(AfraAppClaimTypes.GlobalPermission, - nameof(GlobalPermission.Profundumsverantwortlich))) - .AddPolicy(AuthorizationPolicies.AdminOnly, - policy => policy.RequireClaim(AfraAppClaimTypes.GlobalPermission, - nameof(GlobalPermission.Admin))) - .AddPolicy(AuthorizationPolicies.TeacherOrAdmin, - policy => policy.RequireAssertion(context => - context.User.HasClaim(AfraAppClaimTypes.GlobalPermission, nameof(GlobalPermission.Admin)) - || context.User.HasClaim(AfraAppClaimTypes.Role, nameof(Rolle.Tutor)))); - } - - /// - public void RegisterMiddleware(WebApplication app) - { - app.UseAuthorization(); - } -} diff --git a/Backend/Altafraner.Backbone.CookieAuthentication/CookieAuthenticationSettings.cs b/Backend/Altafraner.AfraApp/Domain/Configuration/CookieAuthenticationSettings.cs similarity index 90% rename from Backend/Altafraner.Backbone.CookieAuthentication/CookieAuthenticationSettings.cs rename to Backend/Altafraner.AfraApp/Domain/Configuration/CookieAuthenticationSettings.cs index f363aafe..3500e60c 100644 --- a/Backend/Altafraner.Backbone.CookieAuthentication/CookieAuthenticationSettings.cs +++ b/Backend/Altafraner.AfraApp/Domain/Configuration/CookieAuthenticationSettings.cs @@ -1,6 +1,4 @@ -using Microsoft.AspNetCore.Http; - -namespace Altafraner.Backbone.CookieAuthentication; +namespace Altafraner.AfraApp.Domain.Configuration; /// /// Settings for handling cookie authentication diff --git a/Backend/Altafraner.AfraApp/Domain/Configuration/OidcConfiguration.cs b/Backend/Altafraner.AfraApp/Domain/Configuration/OidcConfiguration.cs new file mode 100644 index 00000000..8613d00a --- /dev/null +++ b/Backend/Altafraner.AfraApp/Domain/Configuration/OidcConfiguration.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; + +namespace Altafraner.AfraApp.Domain.Configuration; + +/// +/// Contains OIDC Configuration +/// +public class OidcConfiguration +{ + /// + /// Whether OIDC is used + /// + public bool Enabled { get; set; } + + /// + /// The OIDC Authority + /// + /// For keycloak, use https://[your-keycloak-url]/realms/[your-realm] + public string? Authority { get; set; } + + /// + /// The name of the claim that stores the ldap objectGuid + /// + public string? IdClaim { get; set; } + + /// + /// The OIDC Client ID + /// + public string? ClientId { get; set; } + + /// + /// The OIDC Client Secret + /// + public string? ClientSecret { get; set; } + + internal static bool Validate(OidcConfiguration configuration) + { + if (!configuration.Enabled) return true; + if (!string.IsNullOrWhiteSpace(configuration.Authority) + && !string.IsNullOrWhiteSpace(configuration.ClientId) + && !string.IsNullOrWhiteSpace(configuration.ClientSecret) + && !string.IsNullOrWhiteSpace(configuration.IdClaim)) + return true; + throw new ValidationException( + $"{nameof(Authority)}, {nameof(ClientId)}, {nameof(ClientSecret)} and {nameof(IdClaim)} must be set when oidc is enabled"); + } +} diff --git a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Dashboard.cs b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Dashboard.cs index 52ccf395..32a2df46 100644 --- a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Dashboard.cs +++ b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Dashboard.cs @@ -1,4 +1,4 @@ -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Otium.Services; using Altafraner.AfraApp.User.Domain.Models; using Altafraner.AfraApp.User.Services; @@ -48,15 +48,9 @@ private static async Task GetStudentDashboardForTeacher(OtiumEndpointSe Guid studentId, bool all = false) { - Person student; - try - { - student = await userService.GetUserByIdAsync(studentId); - } - catch (KeyNotFoundException) - { + var student = await userService.GetUserByIdAsync(studentId); + if (student is null) return Results.NotFound(); - } var isMentor = await authHelper.CurrentUserIsMentorOf(student); var hasBypass = await authHelper.CurrentUserHasGlobalPermission(GlobalPermission.Otiumsverantwortlich) || diff --git a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Katalog.cs b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Katalog.cs index 4ed862a9..e70e3175 100644 --- a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Katalog.cs +++ b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Katalog.cs @@ -1,4 +1,4 @@ -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Otium.Services; using Altafraner.AfraApp.User.Services; using Microsoft.AspNetCore.Mvc; diff --git a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Management.cs b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Management.cs index c3b318d8..7bde4d30 100644 --- a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Management.cs +++ b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Management.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Otium.Domain.Contracts.Services; using Altafraner.AfraApp.Otium.Domain.DTO; using Altafraner.AfraApp.Otium.Domain.Models; @@ -619,6 +619,7 @@ private static async Task OtiumTerminForceUnenroll(Guid otiumTerminId, return Results.BadRequest("Der Block ist bereits abgeschlossen oder läuft."); var student = await userService.GetUserByIdAsync(personIdWrapper.Value); + if (student is null) return Results.Unauthorized(); await enrollmentService.UnenrollAsync(otiumTerminId, student, true); return Results.Ok(); } diff --git a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Note.cs b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Note.cs index 024a0770..10295df2 100644 --- a/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Note.cs +++ b/Backend/Altafraner.AfraApp/Otium/API/Endpoints/Note.cs @@ -24,6 +24,8 @@ private static async Task AddNote(NotesService service, if (user.Id != request.StudentId && user.Rolle != Rolle.Tutor) return Results.Forbid(); var affected = await userService.GetUserByIdAsync(request.StudentId); + if (affected is null) + return Results.Unauthorized(); if (affected.Rolle is not Rolle.Mittelstufe and not Rolle.Oberstufe) return Results.BadRequest(); var success = await service.TryAddNoteAsync(request.Content, request.StudentId, request.BlockId, user.Id); @@ -40,6 +42,7 @@ private static async Task UpdateNote(NotesService service, if (user.Id != request.StudentId && user.Rolle != Rolle.Tutor) return Results.Forbid(); var affected = await userService.GetUserByIdAsync(request.StudentId); + if (affected is null) return Results.Unauthorized(); if (affected.Rolle is not Rolle.Mittelstufe and not Rolle.Oberstufe) return Results.BadRequest("The person represented by studentId is not a student."); diff --git a/Backend/Altafraner.AfraApp/Otium/API/Hubs/AttendanceHub.cs b/Backend/Altafraner.AfraApp/Otium/API/Hubs/AttendanceHub.cs index 958cd66a..1ec4c1f7 100644 --- a/Backend/Altafraner.AfraApp/Otium/API/Hubs/AttendanceHub.cs +++ b/Backend/Altafraner.AfraApp/Otium/API/Hubs/AttendanceHub.cs @@ -313,6 +313,7 @@ public async Task MoveStudentNow(Guid studentId, try { var student = await userService.GetUserByIdAsync(studentId); + if (student is null) throw new HubException("User not found"); await enrollmentService.ForceMoveNow(studentId, fromData?.Termin.Id ?? Guid.Empty, toTerminId); if (student.Rolle == Rolle.Mittelstufe) { @@ -386,6 +387,8 @@ public async Task MoveStudent(Guid studentId, Guid toTerminId, EnrollmentService try { var student = await userService.GetUserByIdAsync(studentId); + if (student is null) + throw new HubException("Unauthorized"); var (fromTerminId, blockId) = await enrollmentService.ForceMove(studentId, toTerminId); if (student.Rolle == Rolle.Mittelstufe) { @@ -428,6 +431,7 @@ public async Task ForceUnenroll(Guid studentId, throw new HubException("You do not have permission to unenroll students in this block."); var student = await userService.GetUserByIdAsync(studentId); + if (student is null) throw new HubException("Unauthorized"); await enrollmentService.UnenrollAsync(termin.Id, student, true); if (student.Rolle == Rolle.Mittelstufe) { diff --git a/Backend/Altafraner.AfraApp/Otium/OtiumModule.cs b/Backend/Altafraner.AfraApp/Otium/OtiumModule.cs index 22c99af6..09f02f63 100644 --- a/Backend/Altafraner.AfraApp/Otium/OtiumModule.cs +++ b/Backend/Altafraner.AfraApp/Otium/OtiumModule.cs @@ -1,4 +1,4 @@ -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Otium.API.Endpoints; using Altafraner.AfraApp.Otium.API.Hubs; using Altafraner.AfraApp.Otium.Configuration; diff --git a/Backend/Altafraner.AfraApp/Otium/Services/AttendanceService.cs b/Backend/Altafraner.AfraApp/Otium/Services/AttendanceService.cs index 3c1197f9..0c792ffd 100644 --- a/Backend/Altafraner.AfraApp/Otium/Services/AttendanceService.cs +++ b/Backend/Altafraner.AfraApp/Otium/Services/AttendanceService.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Otium.Domain.Contracts.Services; using Altafraner.AfraApp.Otium.Domain.Models; using Altafraner.AfraApp.Otium.Domain.Models.TimeInterval; diff --git a/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Bewertung.cs b/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Bewertung.cs index cd138fcd..019d53f2 100644 --- a/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Bewertung.cs +++ b/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Bewertung.cs @@ -1,5 +1,5 @@ using System.Net.Mime; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Profundum.Domain.DTO; using Altafraner.AfraApp.Profundum.Services; using Altafraner.AfraApp.User.Services; @@ -51,6 +51,8 @@ public static void MapBewertungEndpoints(this IEndpointRouteBuilder app) DateOnly ausgabedatum) => { var user = await userService.GetUserByIdAsync(userId); + if (user is null) + return (Results)TypedResults.Unauthorized(); var fileContents = await profundumManagementService.GenerateFileForPerson(user, schuljahr, halbjahr, ausgabedatum); return TypedResults.File(fileContents, MediaTypeNames.Application.Pdf); diff --git a/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Enrollment.cs b/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Enrollment.cs index a3438d51..3404a9be 100644 --- a/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Enrollment.cs +++ b/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Enrollment.cs @@ -1,4 +1,4 @@ -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Profundum.Services; using Altafraner.AfraApp.User.Services; using Microsoft.EntityFrameworkCore; diff --git a/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Management.cs b/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Management.cs index 24d40e49..edffb702 100644 --- a/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Management.cs +++ b/Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Management.cs @@ -1,6 +1,6 @@ using System.Net.Mime; using System.Text; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Otium.API.Endpoints; using Altafraner.AfraApp.Profundum.Domain.DTO; using Altafraner.AfraApp.Profundum.Domain.Models; diff --git a/Backend/Altafraner.AfraApp/Program.cs b/Backend/Altafraner.AfraApp/Program.cs index 8e5c7102..8212a94d 100644 --- a/Backend/Altafraner.AfraApp/Program.cs +++ b/Backend/Altafraner.AfraApp/Program.cs @@ -1,6 +1,6 @@ using System.Globalization; using Altafraner.AfraApp; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Backbone.EmergencyBackup; using Altafraner.AfraApp.Calendar; using Altafraner.AfraApp.Domain; @@ -10,7 +10,6 @@ using Altafraner.AfraApp.User; using Altafraner.AfraApp.User.Domain.Models; using Altafraner.Backbone; -using Altafraner.Backbone.CookieAuthentication; using Altafraner.Backbone.DataProtection; using Altafraner.Backbone.Defaults; using Altafraner.Backbone.EmailOutbox; @@ -30,10 +29,9 @@ .AddModule() .AddModule() .AddModule() - .AddModule() + .AddModule() .AddModule() // Backbone modules - .AddModule() .AddModule>() .AddModule() .AddModuleAndConfigure, EmailSchedulingSettings>(settings => diff --git a/Backend/Altafraner.AfraApp/Schuljahr/API/Endpoints/Schuljahr.cs b/Backend/Altafraner.AfraApp/Schuljahr/API/Endpoints/Schuljahr.cs index f7dd3c97..fafb4d6d 100644 --- a/Backend/Altafraner.AfraApp/Schuljahr/API/Endpoints/Schuljahr.cs +++ b/Backend/Altafraner.AfraApp/Schuljahr/API/Endpoints/Schuljahr.cs @@ -1,4 +1,4 @@ -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.Otium.Services; using Altafraner.AfraApp.Schuljahr.Domain.DTO; using Altafraner.AfraApp.Schuljahr.Services; diff --git a/Backend/Altafraner.AfraApp/User/API/Endpoints/People.cs b/Backend/Altafraner.AfraApp/User/API/Endpoints/People.cs index a3bbda41..2626d8e8 100644 --- a/Backend/Altafraner.AfraApp/User/API/Endpoints/People.cs +++ b/Backend/Altafraner.AfraApp/User/API/Endpoints/People.cs @@ -1,4 +1,4 @@ -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.User.Domain.DTO; using Altafraner.AfraApp.User.Services; using Microsoft.AspNetCore.Http.HttpResults; @@ -43,6 +43,8 @@ private static async Task GetPersonMentors(AfraAppContext dbContext, Us try { var student = await userService.GetUserByIdAsync(id); + if (student is null) + return Results.Unauthorized(); var mentors = await userService.GetMentorsAsync(student); return Results.Ok(mentors.Select(s => new PersonInfoMinimal(s))); } diff --git a/Backend/Altafraner.AfraApp/User/API/Endpoints/User.cs b/Backend/Altafraner.AfraApp/User/API/Endpoints/User.cs index 54a3559a..7bdc4334 100644 --- a/Backend/Altafraner.AfraApp/User/API/Endpoints/User.cs +++ b/Backend/Altafraner.AfraApp/User/API/Endpoints/User.cs @@ -1,8 +1,7 @@ using System.Security.Claims; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.User.Domain.DTO; using Altafraner.AfraApp.User.Services; -using Altafraner.Backbone.CookieAuthentication; namespace Altafraner.AfraApp.User.API.Endpoints; diff --git a/Backend/Altafraner.AfraApp/User/Services/UserAccessor.cs b/Backend/Altafraner.AfraApp/User/Services/UserAccessor.cs index c124c9ad..1fe9ace2 100644 --- a/Backend/Altafraner.AfraApp/User/Services/UserAccessor.cs +++ b/Backend/Altafraner.AfraApp/User/Services/UserAccessor.cs @@ -1,7 +1,6 @@ using System.Security.Claims; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.User.Domain.Models; -using Altafraner.Backbone.CookieAuthentication; namespace Altafraner.AfraApp.User.Services; diff --git a/Backend/Altafraner.AfraApp/User/Services/UserService.cs b/Backend/Altafraner.AfraApp/User/Services/UserService.cs index ff48d5b0..0309119a 100644 --- a/Backend/Altafraner.AfraApp/User/Services/UserService.cs +++ b/Backend/Altafraner.AfraApp/User/Services/UserService.cs @@ -1,7 +1,5 @@ -using Altafraner.AfraApp.User.Configuration.LDAP; using Altafraner.AfraApp.User.Domain.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; namespace Altafraner.AfraApp.User.Services; @@ -11,32 +9,33 @@ namespace Altafraner.AfraApp.User.Services; public class UserService { private readonly AfraAppContext _dbContext; - private readonly LdapConfiguration _ldapConfiguration; /// /// Called by DI /// - public UserService(AfraAppContext dbContext, IOptions ldapConfiguration) + public UserService(AfraAppContext dbContext) { _dbContext = dbContext; - _ldapConfiguration = ldapConfiguration.Value; } /// /// Gets a user by their ID. /// /// The users Person entity - public async Task GetUserByIdAsync(Guid userId) + public async Task GetUserByIdAsync(Guid userId) { - try - { - return await _dbContext.Personen - .FirstAsync(p => p.Id == userId); - } - catch (InvalidOperationException) - { - throw new KeyNotFoundException("User not found."); - } + return await _dbContext.Personen + .FirstOrDefaultAsync(p => p.Id == userId); + } + + /// + /// Gets a user by their LDAP ID. + /// + /// The users Person entity + public async Task GetUserByLdapIdAsync(Guid ldapId) + { + return await _dbContext.Personen + .FirstOrDefaultAsync(p => p.LdapObjectId == ldapId); } /// diff --git a/Backend/Altafraner.AfraApp/User/Services/UserSigninService.cs b/Backend/Altafraner.AfraApp/User/Services/UserSigninService.cs index 89f31e69..34bfc3bc 100644 --- a/Backend/Altafraner.AfraApp/User/Services/UserSigninService.cs +++ b/Backend/Altafraner.AfraApp/User/Services/UserSigninService.cs @@ -1,8 +1,7 @@ using System.Security.Claims; -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.User.Domain.Models; using Altafraner.AfraApp.User.Services.LDAP; -using Altafraner.Backbone.CookieAuthentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; @@ -103,7 +102,7 @@ private async Task SignInAsync(Models_Person user, bool rememberMe, Guid? impers /// /// The user to generate the for. /// A for the user - private static List GenerateClaims(Models_Person user) + internal static List GenerateClaims(Models_Person user) { var claims = new List { diff --git a/Backend/Altafraner.AfraApp/User/UserModule.cs b/Backend/Altafraner.AfraApp/User/UserModule.cs index 338897bf..e0bb708f 100644 --- a/Backend/Altafraner.AfraApp/User/UserModule.cs +++ b/Backend/Altafraner.AfraApp/User/UserModule.cs @@ -1,4 +1,4 @@ -using Altafraner.AfraApp.Backbone.Authorization; +using Altafraner.AfraApp.Backbone.Auth; using Altafraner.AfraApp.User.API.Endpoints; using Altafraner.AfraApp.User.Configuration.LDAP; using Altafraner.AfraApp.User.Services; @@ -11,7 +11,7 @@ namespace Altafraner.AfraApp.User; /// A module for handling users /// [DependsOn] -[DependsOn] +[DependsOn] public class UserModule : IModule { /// diff --git a/Backend/Altafraner.AfraApp/appsettings.Development.json b/Backend/Altafraner.AfraApp/appsettings.Development.json index ff2fa633..4529e40b 100644 --- a/Backend/Altafraner.AfraApp/appsettings.Development.json +++ b/Backend/Altafraner.AfraApp/appsettings.Development.json @@ -22,6 +22,13 @@ "CookieAuthentication": { "CookieTimeout": "21.00:00:00" }, + "Oidc": { + "Enabled": true, + "Authority": "", + "ClientId": "", + "ClientSecret": "", + "IdClaim": "" + }, "SMTP": { "Host": "localhost", "Port": "1025", diff --git a/Backend/Altafraner.Backbone.CookieAuthentication/Altafraner.Backbone.CookieAuthentication.csproj b/Backend/Altafraner.Backbone.CookieAuthentication/Altafraner.Backbone.CookieAuthentication.csproj deleted file mode 100644 index 6537ab4c..00000000 --- a/Backend/Altafraner.Backbone.CookieAuthentication/Altafraner.Backbone.CookieAuthentication.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Backend/Altafraner.Backbone.CookieAuthentication/CookieAuthenticationModule.cs b/Backend/Altafraner.Backbone.CookieAuthentication/CookieAuthenticationModule.cs deleted file mode 100644 index 4a2fa084..00000000 --- a/Backend/Altafraner.Backbone.CookieAuthentication/CookieAuthenticationModule.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Altafraner.Backbone.Abstractions; -using Altafraner.Backbone.CookieAuthentication.Services; -using Altafraner.Backbone.Defaults; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Altafraner.Backbone.CookieAuthentication; - -/// -/// A module handling cookie authentication -/// -[DependsOn] -public class CookieAuthenticationModule : IModule -{ - /// - public void ConfigureServices(IServiceCollection services, IConfiguration config, IHostEnvironment env) - { - var section = config.GetSection("CookieAuthentication"); - services.AddOptions().Bind(section); - - var settings = section.Exists() - ? section.Get() ?? - throw new ValidationException("Cannot bind CookieAuthenticationSettings") - : new CookieAuthenticationSettings(); - - services.AddAuthentication() - .AddCookie(options => - { - options.ExpireTimeSpan = settings.CookieTimeout; - options.Cookie.SameSite = settings.SameSiteMode; - options.Cookie.SecurePolicy = settings.SecurePolicy; - options.SlidingExpiration = settings.SlidingExpiration; - }); - services.AddScoped(); - } - - /// - public void RegisterMiddleware(WebApplication app) - { - app.UseAuthentication(); - } -} diff --git a/Backend/Directory.Packages.props b/Backend/Directory.Packages.props index 815030f1..acd5b180 100644 --- a/Backend/Directory.Packages.props +++ b/Backend/Directory.Packages.props @@ -6,6 +6,7 @@ + @@ -28,4 +29,4 @@ - \ No newline at end of file + diff --git a/WebClient/src/App.vue b/WebClient/src/App.vue index b68b7975..c466ee09 100644 --- a/WebClient/src/App.vue +++ b/WebClient/src/App.vue @@ -5,27 +5,23 @@ import 'primeicons/primeicons.css'; import DynamicDialog from 'primevue/dynamicdialog'; import AfraNav from '@/components/AfraNav.vue'; import { useUser } from '@/stores/user'; -import { computed } from 'vue'; -import wappenLight from '/vdaa/favicon.svg?url'; -import wappenDark from '/vdaa/favicon-dark.svg?url'; -import { ConfirmPopup, Image, Skeleton, Toast, useToast } from 'primevue'; -import Login from '@/components/Login.vue'; -import { isDark } from '@/helpers/isdark'; +import { useUserManagement } from '@/composables/userManagement.ts'; +import { ConfirmPopup, Toast, useToast } from 'primevue'; import ReloadPrompt from '@/components/ReloadPrompt.vue'; import { useRoute } from 'vue-router'; +import SkeletonView from '@/components/SkeletonView.vue'; +const userManagement = useUserManagement(); const route = useRoute(); const user = useUser(); const toast = useToast(); -user.update().catch(() => { +userManagement.updateUser().catch(() => { toast.add({ severity: 'error', summary: 'Fehler', detail: 'Ein unerwarteter Fehler ist beim Laden der Nutzerdaten aufgetreten', }); }); - -const logo = computed(() => (isDark().value ? wappenDark : wappenLight)); diff --git a/WebClient/src/components/Login.vue b/WebClient/src/components/Login.vue index aca7012c..260dcc39 100644 --- a/WebClient/src/components/Login.vue +++ b/WebClient/src/components/Login.vue @@ -1,78 +1,38 @@ - diff --git a/WebClient/src/components/SkeletonView.vue b/WebClient/src/components/SkeletonView.vue new file mode 100644 index 00000000..24f50c2d --- /dev/null +++ b/WebClient/src/components/SkeletonView.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/WebClient/src/composables/userManagement.ts b/WebClient/src/composables/userManagement.ts new file mode 100644 index 00000000..3cb5d071 --- /dev/null +++ b/WebClient/src/composables/userManagement.ts @@ -0,0 +1,55 @@ +import { useUser } from '@/stores/user'; +import { useRoute, useRouter } from 'vue-router'; +import { mande } from 'mande'; +import { registerLoggedInRoutes, registerLoggedOutRoutes } from '@/router/index'; + +export function useUserManagement() { + const userStore = useUser(); + const router = useRouter(); + const route = useRoute(); + + async function updateUser() { + const fetchUser = mande('/api/user'); + + const wasLoggedIn = userStore.loggedIn; + + const userPromise = fetchUser.get(); + try { + userStore.user = await userPromise; + userStore.loggedIn = true; + if (!wasLoggedIn) { + registerLoggedInRoutes(router); + // we need to force vue router to reevaluate which route we're on since the route for our current url might have changed with logging in. + await router.replace(route.fullPath); + } + } catch (error) { + if (error.response.status === 401) { + userStore.loggedIn = false; + userStore.user = null; + console.info('Not logged in'); + registerLoggedOutRoutes(router); + } else { + console.error('Error fetching user', error); + throw error; + } + } finally { + userStore.loading = false; + } + } + + async function logout() { + const logoutUser = mande('/api/user/logout'); + await logoutUser.get(); + registerLoggedOutRoutes(router); + userStore.loggedIn = false; + userStore.user = null; + await router.replace(route.fullPath); + } + + async function impersonateUser(userId: string) { + await mande(`/api/user/${userId}/impersonate`).get(); + await updateUser(); + } + + return { updateUser, logout, impersonateUser }; +} diff --git a/WebClient/src/main.js b/WebClient/src/main.js index 2b124436..84ddc617 100644 --- a/WebClient/src/main.js +++ b/WebClient/src/main.js @@ -1,6 +1,6 @@ import { createApp } from 'vue'; import App from './App.vue'; -import router from './router'; +import { createAppRouter } from './router'; import PrimeVue from 'primevue/config'; import ToastService from 'primevue/toastservice'; import Aura from '@primevue/themes/aura'; @@ -167,7 +167,7 @@ const pinia = createPinia(); const app = createApp(App); app.use(pinia); -app.use(router); +app.use(createAppRouter()); app.use(PrimeVue, { theme: { preset: AfraAppPreset, diff --git a/WebClient/src/router/index.js b/WebClient/src/router/index.js index f3195ccf..c28bfeed 100644 --- a/WebClient/src/router/index.js +++ b/WebClient/src/router/index.js @@ -2,8 +2,38 @@ import { createRouter, createWebHistory } from 'vue-router'; import Home from '@/views/Home.vue'; import { routes as otium } from '@/Otium/router/routes.js'; import { routes as profundum } from '@/Profundum/router/routes.js'; +import LoggedOutHome from '@/views/LoggedOutHome.vue'; +import AccessDenied from '@/views/oidc/AccessDenied.vue'; +import RemoteFailure from '@/views/oidc/RemoteFailure.vue'; -const routes = [ +const loggedOutRoutes = [ + { + path: '/', + name: 'Home', + component: LoggedOutHome, + }, + { + path: '/oidc', + children: [ + { + name: 'oidc-access-denied', + path: 'access-denied', + component: AccessDenied, + }, + { + name: 'oidc-error', + path: 'remote-error', + component: RemoteFailure, + }, + ], + }, + { + path: '/:pathMatch(?!api/)(.*)*', + name: 'NotFound', + component: LoggedOutHome, + }, +]; +const loggedInRoutes = [ { path: '/', name: 'Home', @@ -28,9 +58,32 @@ const routes = [ }, ]; -const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes, -}); +export function createAppRouter() { + return createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: loggedOutRoutes, + }); +} + +function removeRoutes(router, routes) { + for (const route of routes) { + if (route.name && router.hasRoute(route.name)) { + router.removeRoute(route.name); + } + } +} + +function addRoutes(router, routes) { + for (const route of routes) { + router.addRoute(route); + } +} + +export function registerLoggedInRoutes(router) { + addRoutes(router, loggedInRoutes); +} -export default router; +export function registerLoggedOutRoutes(router) { + removeRoutes(router, loggedInRoutes); + addRoutes(router, loggedOutRoutes); +} diff --git a/WebClient/src/stores/user.ts b/WebClient/src/stores/user.ts index 60014ab8..cb51e162 100644 --- a/WebClient/src/stores/user.ts +++ b/WebClient/src/stores/user.ts @@ -1,5 +1,4 @@ import { defineStore } from 'pinia'; -import { mande } from 'mande'; export const useUser = defineStore('user', { state: () => ({ @@ -19,33 +18,4 @@ export const useUser = defineStore('user', { isAdmin: (state) => state.user.berechtigungen.includes('Admin'), isImpersonating: (state) => state.user?.impersonationId != null, }, - actions: { - async update() { - const fetchUser = mande('/api/user'); - - const userPromise = fetchUser.get(); - try { - this.user = await userPromise; - this.loggedIn = true; - } catch (error) { - if (error.response.status === 401) { - this.loggedIn = false; - this.user = null; - console.info('Not logged in'); - } else { - console.error('Error fetching user', error); - throw error; - } - } finally { - this.loading = false; - } - }, - - async logout() { - const logoutUser = mande('/api/user/logout'); - await logoutUser.get(); - this.loggedIn = false; - this.user = null; - }, - }, }); diff --git a/WebClient/src/views/Admin.vue b/WebClient/src/views/Admin.vue index 97069ce7..abe16040 100644 --- a/WebClient/src/views/Admin.vue +++ b/WebClient/src/views/Admin.vue @@ -4,11 +4,12 @@ import { useOtiumStore } from '@/Otium/stores/otium.js'; import { computed } from 'vue'; import { formatTutor } from '@/helpers/formatters'; import { Button } from 'primevue'; -import { mande } from 'mande'; import { useRouter } from 'vue-router'; import UserPeek from '@/components/UserPeek.vue'; +import { useUserManagement } from '@/composables/userManagement.ts'; const user = useUser(); +const userManagement = useUserManagement(); const otium = useOtiumStore(); const router = useRouter(); @@ -58,9 +59,7 @@ const personen = computed(() => { }); const impersonate = async (userToImpersonate) => { - console.log(userToImpersonate); - await mande(`/api/user/${userToImpersonate.id}/impersonate`).get(); - await user.update(); + await userManagement.impersonateUser(userToImpersonate.id); await router.push('/'); }; diff --git a/WebClient/src/views/LoggedOutHome.vue b/WebClient/src/views/LoggedOutHome.vue new file mode 100644 index 00000000..d3223c66 --- /dev/null +++ b/WebClient/src/views/LoggedOutHome.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/WebClient/src/views/Settings.vue b/WebClient/src/views/Settings.vue index dc922704..6a662da2 100644 --- a/WebClient/src/views/Settings.vue +++ b/WebClient/src/views/Settings.vue @@ -4,9 +4,11 @@ import { ref } from 'vue'; import { mande } from 'mande'; import { useUser } from '@/stores/user'; import NavBreadcrumb from '@/components/NavBreadcrumb.vue'; +import { useUserManagement } from '@/composables/userManagement.ts'; const loading = ref(false); const user = useUser(); +const userManagement = useUserManagement(); const toast = useToast(); const calLink = ref(null); @@ -18,7 +20,7 @@ async function fetchNum() { try { numSubs.value = await endpoint.get(); } catch (e) { - await user.update(); + await userManagement.updateUser(); toast.add({ severity: 'error', summary: 'Fehler', @@ -36,7 +38,7 @@ async function fetchKey() { try { calLink.value = await endpoint.get(); } catch (e) { - await user.update(); + await userManagement.updateUser(); toast.add({ severity: 'error', summary: 'Fehler', @@ -62,7 +64,7 @@ async function deleteKeys() { life: 2000, }); } catch (e) { - await user.update(); + await userManagement.updateUser(); toast.add({ severity: 'error', summary: 'Fehler', diff --git a/WebClient/src/views/oidc/AccessDenied.vue b/WebClient/src/views/oidc/AccessDenied.vue new file mode 100644 index 00000000..33e82b94 --- /dev/null +++ b/WebClient/src/views/oidc/AccessDenied.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/WebClient/src/views/oidc/RemoteFailure.vue b/WebClient/src/views/oidc/RemoteFailure.vue new file mode 100644 index 00000000..e3a47620 --- /dev/null +++ b/WebClient/src/views/oidc/RemoteFailure.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/WebClient/vite.config.js b/WebClient/vite.config.js index c95c5f94..0c3fd849 100644 --- a/WebClient/vite.config.js +++ b/WebClient/vite.config.js @@ -55,8 +55,11 @@ export default defineConfig({ proxy: { '/api': { target: 'http://127.0.0.1:5043', - changeOrigin: true, + changeOrigin: false, ws: true, + xfwd: true, + hostRewrite: true, + cookieDomainRewrite: 'localhost:5173', }, }, },