Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions Backend/Afra-App.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
<Folder Name="/Solution Items/">
<File Path="../compose.yaml" />
</Folder>
<Project Path="Altafraner.AfraApp\Altafraner.AfraApp.csproj" Type="C#" />
<Project Path="Altafraner.AfraApp\Altafraner.AfraApp.csproj"/>
<Project Path="Altafraner.Typst.Generator/Altafraner.Typst.Generator.csproj" />
<Project Path="Altafraner.Typst\Altafraner.Typst.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.Abstractions\Altafraner.Backbone.Abstractions.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.CookieAuthentication\Altafraner.Backbone.CookieAuthentication.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.DataProtection\Altafraner.Backbone.DataProtection.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.Defaults\Altafraner.Backbone.Defaults.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.EmailOutbox\Altafraner.Backbone.EmailOutbox.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.EmailSchedulingModule\Altafraner.Backbone.EmailSchedulingModule.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.Scheduling\Altafraner.Backbone.Scheduling.csproj" Type="C#" />
<Project Path="Altafraner.Backbone.Utils\Altafraner.Backbone.Utils.csproj" Type="C#" />
<Project Path="Altafraner.Backbone\Altafraner.Backbone.csproj" Type="C#" />
<Project Path="Altafraner.Typst\Altafraner.Typst.csproj"/>
<Project Path="Altafraner.Backbone.Abstractions\Altafraner.Backbone.Abstractions.csproj"/>
<Project Path="Altafraner.Backbone.DataProtection\Altafraner.Backbone.DataProtection.csproj"/>
<Project Path="Altafraner.Backbone.Defaults\Altafraner.Backbone.Defaults.csproj"/>
<Project Path="Altafraner.Backbone.EmailOutbox\Altafraner.Backbone.EmailOutbox.csproj"/>
<Project Path="Altafraner.Backbone.EmailSchedulingModule\Altafraner.Backbone.EmailSchedulingModule.csproj"/>
<Project Path="Altafraner.Backbone.Scheduling\Altafraner.Backbone.Scheduling.csproj"/>
<Project Path="Altafraner.Backbone.Utils\Altafraner.Backbone.Utils.csproj"/>
<Project Path="Altafraner.Backbone\Altafraner.Backbone.csproj"/>
</Solution>
2 changes: 1 addition & 1 deletion Backend/Altafraner.AfraApp/Altafraner.AfraApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Ical.Net"/>
<PackageReference Include="MailKit"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect"/>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore"/>
<PackageReference Include="Bogus"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi"/>
Expand Down Expand Up @@ -42,7 +43,6 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Altafraner.Backbone.CookieAuthentication\Altafraner.Backbone.CookieAuthentication.csproj"/>
<ProjectReference Include="..\Altafraner.Backbone.DataProtection\Altafraner.Backbone.DataProtection.csproj"/>
<ProjectReference Include="..\Altafraner.Backbone.Utils\Altafraner.Backbone.Utils.csproj"/>
<ProjectReference Include="..\Altafraner.Backbone.EmailOutbox\Altafraner.Backbone.EmailOutbox.csproj"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Security.Claims;
using Altafraner.AfraApp.User.Domain.Models;

namespace Altafraner.AfraApp.Backbone.Authorization;
namespace Altafraner.AfraApp.Backbone.Auth;

/// <summary>
/// Specifies the claim types used in the <see cref="ClaimsPrincipal" /> for the Afra-App
Expand Down
180 changes: 180 additions & 0 deletions Backend/Altafraner.AfraApp/Backbone/Auth/AuthModule.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A module for handling simple authorization cases
/// </summary>
[DependsOn<ReverseProxyHandlerModule>]
internal class AuthModule : IModule
{
public void ConfigureServices(IServiceCollection services, IConfiguration config, IHostEnvironment env)
{
var cookieSection = config.GetSection("CookieAuthentication");
services.AddOptions<CookieAuthenticationSettings>().Bind(cookieSection);

var cookieSettings = cookieSection.Exists()
? cookieSection.Get<CookieAuthenticationSettings>() ??
throw new ValidationException("Cannot bind CookieAuthenticationSettings")
: new CookieAuthenticationSettings();

var oidcSection = config.GetSection("Oidc");
services.AddOptions<OidcConfiguration>()
.Validate(OidcConfiguration.Validate)
.ValidateOnStart()
.Bind(oidcSection);


var oidcSettings = oidcSection.Exists()
? oidcSection.Get<OidcConfiguration>() ??
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<ILogger<AuthModule>>();
logger.LogWarning("OIDC Access Denied");
context.Response.Redirect("/oidc/access-denied");
context.HandleResponse();
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<AuthModule>>();
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<ILogger<OpenIdConnectEvents>>();
var oidcSettings = context.HttpContext.RequestServices.GetRequiredService<IOptions<OidcConfiguration>>().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<UserService>();

var user = await userService.GetUserByLdapIdAsync(new Guid(userId));
if (user is null)
{
var ldapService = context.HttpContext.RequestServices.GetRequiredService<LdapService>();
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]));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Altafraner.AfraApp.Backbone.Authorization;
namespace Altafraner.AfraApp.Backbone.Auth;

/// <summary>
/// A static class containing constants for authorization policies.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Security.Claims;

namespace Altafraner.Backbone.CookieAuthentication;
namespace Altafraner.AfraApp.Backbone.Auth;

/// <summary>
/// A service handling signing in and out users.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Http;

namespace Altafraner.Backbone.CookieAuthentication;
namespace Altafraner.AfraApp.Domain.Configuration;

/// <summary>
/// Settings for handling cookie authentication
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations;

namespace Altafraner.AfraApp.Domain.Configuration;

/// <summary>
/// Contains OIDC Configuration
/// </summary>
public class OidcConfiguration
{
/// <summary>
/// Whether OIDC is used
/// </summary>
public bool Enabled { get; set; }

/// <summary>
/// The OIDC Authority
/// </summary>
/// <example>For keycloak, use <c>https://[your-keycloak-url]/realms/[your-realm]</c></example>
public string? Authority { get; set; }

/// <summary>
/// The name of the claim that stores the ldap objectGuid
/// </summary>
public string? IdClaim { get; set; }

/// <summary>
/// The OIDC Client ID
/// </summary>
public string? ClientId { get; set; }

/// <summary>
/// The OIDC Client Secret
/// </summary>
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");
}
}
12 changes: 3 additions & 9 deletions Backend/Altafraner.AfraApp/Otium/API/Endpoints/Dashboard.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -48,15 +48,9 @@ private static async Task<IResult> 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) ||
Expand Down
2 changes: 1 addition & 1 deletion Backend/Altafraner.AfraApp/Otium/API/Endpoints/Katalog.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading
Loading