From 951c5e45fae21b8764031eda7d192124943f93d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Dybvik=20Langfors?= Date: Sun, 14 Apr 2024 11:05:46 +0200 Subject: [PATCH 1/3] Generalize user authz handling, add support for legacy enterprise users --- .../Extensions/ClaimsPrincipalExtensions.cs | 58 +++++++++++++++++++ .../Common/IUserNameRegistry.cs | 44 +++++++++++--- .../Externals/Authentication/UserType.cs | 10 ++++ .../Queries/Get/GetDialogSeenLogQuery.cs | 4 +- .../Search/SearchDialogSeenLogQuery.cs | 4 +- .../Dialogs/Queries/Get/GetDialogQuery.cs | 2 +- .../Queries/Search/SearchDialogQuery.cs | 4 +- 7 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs index 2ecf6c5b6..cbdf35188 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Digdir.Domain.Dialogporten.Application.Externals.Authentication; using Digdir.Domain.Dialogporten.Domain.Parties; namespace Digdir.Domain.Dialogporten.Application.Common.Extensions; @@ -19,6 +20,10 @@ public static class ClaimsPrincipalExtensions private const string OrgClaim = "urn:altinn:org"; private const string IdportenAuthLevelClaim = "acr"; private const string AltinnAuthLevelClaim = "urn:altinn:authlevel"; + private const string AltinnAuthenticationMethodClaim = "urn:altinn:authenticatemethod"; + private const string AltinnAuthenticationEnterpriseUserMethod = "virksomhetsbruker"; + private const string AltinnUserIdClaim = "urn:altinn:userid"; + private const string AltinnUserNameClaim = "urn:altinn:username"; private const string PidClaim = "pid"; public static bool TryGetClaimValue(this ClaimsPrincipal claimsPrincipal, string claimType, [NotNullWhen(true)] out string? value) @@ -52,6 +57,33 @@ public static bool TryGetPid(this Claim? pidClaim, [NotNullWhen(true)] out strin return pid is not null; } + // This is used for legacy systems using Altinn 2 enterprise users with Maskinporten authentication + token exchange + // as described in https://altinn.github.io/docs/api/rest/kom-i-gang/virksomhet/#autentisering-med-virksomhetsbruker-og-maskinporten + public static bool TryGetLegacySystemUserId(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? systemUserId) + { + systemUserId = null; + if (claimsPrincipal.TryGetClaimValue(AltinnAuthenticationMethodClaim, out var authMethod) && + authMethod == AltinnAuthenticationEnterpriseUserMethod && + claimsPrincipal.TryGetClaimValue(AltinnUserIdClaim, out var userId)) + { + systemUserId = userId; + } + + return systemUserId is not null; + } + + public static bool TryGetLegacySystemUserName(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? systemUserName) + { + systemUserName = null; + if (claimsPrincipal.TryGetLegacySystemUserId(out _) && + claimsPrincipal.TryGetClaimValue(AltinnUserNameClaim, out var claimValue)) + { + systemUserName = claimValue; + } + + return systemUserName is not null; + } + public static bool TryGetOrgNumber(this Claim? consumerClaim, [NotNullWhen(true)] out string? orgNumber) { orgNumber = null; @@ -114,6 +146,26 @@ public static IEnumerable GetIdentifyingClaims(this List claims) = c.Type.StartsWith(AltinnClaimPrefix, StringComparison.Ordinal) ).OrderBy(c => c.Type); + public static UserType GetUserType(this ClaimsPrincipal claimsPrincipal) + { + if (claimsPrincipal.TryGetPid(out _)) + { + return UserType.Person; + } + + if (claimsPrincipal.TryGetLegacySystemUserId(out _)) + { + return UserType.LegacySystemUser; + } + + if (claimsPrincipal.TryGetOrgNumber(out _)) + { + return UserType.Enterprise; + } + + return UserType.Unknown; + } + private static bool TryGetOrgShortName(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgShortName) => claimsPrincipal.FindFirst(OrgClaim).TryGetOrgShortName(out orgShortName); @@ -132,4 +184,10 @@ internal static bool TryGetOrgShortName(this IUser user, [NotNullWhen(true)] out internal static bool TryGetPid(this IUser user, [NotNullWhen(true)] out string? pid) => user.GetPrincipal().TryGetPid(out pid); + + internal static bool TryGetLegacySystemUserId(this IUser user, [NotNullWhen(true)] out string? systemUserId) => + user.GetPrincipal().TryGetLegacySystemUserId(out systemUserId); + + internal static bool TryGetLegacySystemUserName(this IUser user, [NotNullWhen(true)] out string? systemUserName) => + user.GetPrincipal().TryGetLegacySystemUserName(out systemUserName); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs b/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs index 9e8b06c98..facb4344f 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs @@ -2,13 +2,14 @@ using System.Diagnostics.CodeAnalysis; using Digdir.Domain.Dialogporten.Application.Common.Extensions; using Digdir.Domain.Dialogporten.Application.Externals; +using Digdir.Domain.Dialogporten.Application.Externals.Authentication; using Digdir.Domain.Dialogporten.Application.Externals.Presentation; namespace Digdir.Domain.Dialogporten.Application.Common; public interface IUserNameRegistry { - bool TryGetCurrentUserPid([NotNullWhen(true)] out string? userPid); + bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId); Task GetUserInformation(CancellationToken cancellationToken); } @@ -18,24 +19,49 @@ public class UserNameRegistry : IUserNameRegistry { private readonly IUser _user; private readonly INameRegistry _nameRegistry; + private readonly IOrganizationRegistry _organizationRegistry; - public UserNameRegistry(IUser user, INameRegistry nameRegistry) + public UserNameRegistry(IUser user, INameRegistry nameRegistry, IOrganizationRegistry organizationRegistry) { _user = user ?? throw new ArgumentNullException(nameof(user)); _nameRegistry = nameRegistry ?? throw new ArgumentNullException(nameof(nameRegistry)); + _organizationRegistry = organizationRegistry ?? throw new ArgumentNullException(nameof(organizationRegistry)); } - public bool TryGetCurrentUserPid([NotNullWhen(true)] out string? userPid) => _user.TryGetPid(out userPid); + public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId) + { + if (_user.TryGetPid(out userExternalId)) return true; + if (_user.TryGetLegacySystemUserId(out userExternalId)) return true; + if (_user.TryGetOrgNumber(out userExternalId)) return true; + return false; + } public async Task GetUserInformation(CancellationToken cancellationToken) { - if (!TryGetCurrentUserPid(out var userPid)) + if (!TryGetCurrentUserExternalId(out var userExernalId)) { return null; } - var userName = await _nameRegistry.GetName(userPid, cancellationToken); - return new(userPid, userName); + string? userName; + switch (_user.GetPrincipal().GetUserType()) + { + case UserType.Person: + userName = await _nameRegistry.GetName(userExernalId, cancellationToken); + break; + case UserType.LegacySystemUser: + _user.TryGetLegacySystemUserName(out userName); + break; + case UserType.Enterprise: + userName = await _organizationRegistry.GetOrgShortName(userExernalId, cancellationToken); + break; + case UserType.Unknown: + case UserType.SystemUser: // Implement when we know how this will be handled + default: + throw new UnreachableException("Unknown user type"); + } + + return new(userExernalId, userName); } } @@ -50,11 +76,11 @@ public LocalDevelopmentUserNameRegistryDecorator(IUserNameRegistry userNameRegis _userNameRegistry = userNameRegistry ?? throw new ArgumentNullException(nameof(userNameRegistry)); } - public bool TryGetCurrentUserPid([NotNullWhen(true)] out string? userPid) => - _userNameRegistry.TryGetCurrentUserPid(out userPid); + public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId) => + _userNameRegistry.TryGetCurrentUserExternalId(out userExternalId); public Task GetUserInformation(CancellationToken cancellationToken) - => _userNameRegistry.TryGetCurrentUserPid(out var userPid) + => _userNameRegistry.TryGetCurrentUserExternalId(out var userPid) ? Task.FromResult(new UserInformation(userPid!, LocalDevelopmentUserPid)) : throw new UnreachableException(); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs new file mode 100644 index 000000000..9ace2e8db --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs @@ -0,0 +1,10 @@ +namespace Digdir.Domain.Dialogporten.Application.Externals.Authentication; + +public enum UserType +{ + Unknown = 0, + Person = 1, + LegacySystemUser = 2, + SystemUser = 3, + Enterprise = 4 +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs index 8ecbe1f18..dc1fd17d0 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs @@ -44,9 +44,9 @@ public GetDialogSeenLogQueryHandler( public async Task Handle(GetDialogSeenLogQuery request, CancellationToken cancellationToken) { - if (!_userNameRegistry.TryGetCurrentUserPid(out var userPid)) + if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) { - return new Forbidden("No valid user pid found."); + return new Forbidden("No valid user was authenticated"); } var dialog = await _dbContext.Dialogs diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs index ebeba3c4f..8e5b5930a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs @@ -42,9 +42,9 @@ public SearchDialogSeenLogQueryHandler( public async Task Handle(SearchDialogSeenLogQuery request, CancellationToken cancellationToken) { - if (!_userNameRegistry.TryGetCurrentUserPid(out var userPid)) + if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) { - return new Forbidden("No valid user pid found."); + return new Forbidden("No valid user was authenticated"); } var dialog = await _db.Dialogs diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index 1ac23bad1..27c61b492 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -57,7 +57,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo if (userInformation is null) { - return new Forbidden("No valid user pid found."); + return new Forbidden("No valid user was authenticated"); } var (userPid, userName) = userInformation; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs index f30f26dc8..a075f9ae1 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -136,9 +136,9 @@ public SearchDialogQueryHandler( public async Task Handle(SearchDialogQuery request, CancellationToken cancellationToken) { - if (!_userNameRegistry.TryGetCurrentUserPid(out var userPid)) + if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) { - return new Forbidden("No valid user pid found."); + return new Forbidden("No valid user was authenticated"); } var searchExpression = Expressions.LocalizedSearchExpression(request.Search, request.SearchCultureCode); From 6583f54dabf32ba70022a99dffeedc9ed0df87e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20J=C3=B8rgen=20Skogstad?= Date: Sun, 14 Apr 2024 20:53:13 +0200 Subject: [PATCH 2/3] chore(suggestion): Move forbidden message to common (#635) --- .../Common/Authentication/Constants.cs | 6 ++++++ .../DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs | 3 ++- .../Queries/Search/SearchDialogSeenLogQuery.cs | 5 +++-- .../V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs | 3 ++- .../V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs | 3 ++- 5 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs new file mode 100644 index 000000000..63a55a882 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs @@ -0,0 +1,6 @@ +namespace Digdir.Domain.Dialogporten.Application.Common.Authentication; + +public static class Constants +{ + public const string NoAuthenticatedUser = "No valid user was authenticated"; +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs index dc1fd17d0..933d3c9a0 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs @@ -1,5 +1,6 @@ using AutoMapper; using Digdir.Domain.Dialogporten.Application.Common; +using Digdir.Domain.Dialogporten.Application.Common.Authentication; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; @@ -46,7 +47,7 @@ public async Task Handle(GetDialogSeenLogQuery request, { if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) { - return new Forbidden("No valid user was authenticated"); + return new Forbidden(Constants.NoAuthenticatedUser); } var dialog = await _dbContext.Dialogs diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs index 8e5b5930a..6fedd73ed 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs @@ -1,12 +1,13 @@ using AutoMapper; +using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; +using Digdir.Domain.Dialogporten.Application.Common.Authentication; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; using MediatR; using OneOf; using Microsoft.EntityFrameworkCore; -using Digdir.Domain.Dialogporten.Application.Common; namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.DialogSeenLogs.Queries.Search; @@ -44,7 +45,7 @@ public async Task Handle(SearchDialogSeenLogQuery req { if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) { - return new Forbidden("No valid user was authenticated"); + return new Forbidden(Constants.NoAuthenticatedUser); } var dialog = await _db.Dialogs diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index 27c61b492..254e69a72 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -9,6 +9,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; using OneOf; +using AuthenticationConstants = Digdir.Domain.Dialogporten.Application.Common.Authentication.Constants; namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get; @@ -57,7 +58,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo if (userInformation is null) { - return new Forbidden("No valid user was authenticated"); + return new Forbidden(AuthenticationConstants.NoAuthenticatedUser); } var (userPid, userName) = userInformation; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs index a075f9ae1..c3bfa6ea5 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -1,6 +1,7 @@ using AutoMapper; using AutoMapper.QueryableExtensions; using Digdir.Domain.Dialogporten.Application.Common; +using Digdir.Domain.Dialogporten.Application.Common.Authentication; using Digdir.Domain.Dialogporten.Application.Common.Extensions; using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables; using Digdir.Domain.Dialogporten.Application.Common.Pagination; @@ -138,7 +139,7 @@ public async Task Handle(SearchDialogQuery request, Cancella { if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) { - return new Forbidden("No valid user was authenticated"); + return new Forbidden(Constants.NoAuthenticatedUser); } var searchExpression = Expressions.LocalizedSearchExpression(request.Search, request.SearchCultureCode); From e2a351786d61705c055a8f5d2f259b5b0a62827e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Dybvik=20Langfors?= Date: Mon, 15 Apr 2024 11:22:36 +0200 Subject: [PATCH 3/3] Add user type validation middleware --- .../Common/Authentication/Constants.cs | 6 --- .../Common/IUserNameRegistry.cs | 40 +++++++---------- .../Externals/Authentication/UserType.cs | 1 + .../Queries/Get/GetDialogSeenLogQuery.cs | 9 +--- .../Search/SearchDialogSeenLogQuery.cs | 9 +--- .../Dialogs/Queries/Get/GetDialogQuery.cs | 14 ++---- .../Queries/Search/SearchDialogQuery.cs | 9 +--- .../UserTypeValidationMiddleware.cs | 45 +++++++++++++++++++ .../Common/Constants.cs | 1 - .../Program.cs | 1 + 10 files changed, 73 insertions(+), 62 deletions(-) delete mode 100644 src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs create mode 100644 src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs deleted file mode 100644 index 63a55a882..000000000 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Authentication/Constants.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Digdir.Domain.Dialogporten.Application.Common.Authentication; - -public static class Constants -{ - public const string NoAuthenticatedUser = "No valid user was authenticated"; -} diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs b/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs index facb4344f..ab9fa9b3c 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/IUserNameRegistry.cs @@ -9,8 +9,8 @@ namespace Digdir.Domain.Dialogporten.Application.Common; public interface IUserNameRegistry { - bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId); - Task GetUserInformation(CancellationToken cancellationToken); + string GetCurrentUserExternalId(); + Task GetCurrentUserInformation(CancellationToken cancellationToken); } public record UserInformation(string UserPid, string? UserName); @@ -27,22 +27,18 @@ public UserNameRegistry(IUser user, INameRegistry nameRegistry, IOrganizationReg _nameRegistry = nameRegistry ?? throw new ArgumentNullException(nameof(nameRegistry)); _organizationRegistry = organizationRegistry ?? throw new ArgumentNullException(nameof(organizationRegistry)); } - - public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId) + public string GetCurrentUserExternalId() { - if (_user.TryGetPid(out userExternalId)) return true; - if (_user.TryGetLegacySystemUserId(out userExternalId)) return true; - if (_user.TryGetOrgNumber(out userExternalId)) return true; - return false; + if (_user.TryGetPid(out var userId)) return userId; + if (_user.TryGetLegacySystemUserId(out userId)) return userId; + if (_user.TryGetOrgNumber(out userId)) return userId; + + throw new InvalidOperationException("User external id not found"); } - public async Task GetUserInformation(CancellationToken cancellationToken) + public async Task GetCurrentUserInformation(CancellationToken cancellationToken) { - if (!TryGetCurrentUserExternalId(out var userExernalId)) - { - return null; - } - + var userExernalId = GetCurrentUserExternalId(); string? userName; switch (_user.GetPrincipal().GetUserType()) { @@ -55,10 +51,12 @@ public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExte case UserType.Enterprise: userName = await _organizationRegistry.GetOrgShortName(userExernalId, cancellationToken); break; + case UserType.SystemUser: + // TODO: Implement when we know how system users will be handled case UserType.Unknown: - case UserType.SystemUser: // Implement when we know how this will be handled default: - throw new UnreachableException("Unknown user type"); + // This should never happen as GetCurrentExternalId should throw if the user type is unknown + throw new UnreachableException(); } return new(userExernalId, userName); @@ -75,12 +73,8 @@ public LocalDevelopmentUserNameRegistryDecorator(IUserNameRegistry userNameRegis { _userNameRegistry = userNameRegistry ?? throw new ArgumentNullException(nameof(userNameRegistry)); } + public string GetCurrentUserExternalId() => _userNameRegistry.GetCurrentUserExternalId(); - public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId) => - _userNameRegistry.TryGetCurrentUserExternalId(out userExternalId); - - public Task GetUserInformation(CancellationToken cancellationToken) - => _userNameRegistry.TryGetCurrentUserExternalId(out var userPid) - ? Task.FromResult(new UserInformation(userPid!, LocalDevelopmentUserPid)) - : throw new UnreachableException(); + public Task GetCurrentUserInformation(CancellationToken cancellationToken) + => Task.FromResult(new UserInformation(GetCurrentUserExternalId(), LocalDevelopmentUserPid)); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs index 9ace2e8db..955e358a6 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/Authentication/UserType.cs @@ -7,4 +7,5 @@ public enum UserType LegacySystemUser = 2, SystemUser = 3, Enterprise = 4 + // TODO! Should we add a new type for service owners? ServiceownerOnBehalfOfPerson? } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs index 933d3c9a0..fdc95ed32 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Get/GetDialogSeenLogQuery.cs @@ -1,6 +1,5 @@ using AutoMapper; using Digdir.Domain.Dialogporten.Application.Common; -using Digdir.Domain.Dialogporten.Application.Common.Authentication; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; @@ -45,11 +44,7 @@ public GetDialogSeenLogQueryHandler( public async Task Handle(GetDialogSeenLogQuery request, CancellationToken cancellationToken) { - if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) - { - return new Forbidden(Constants.NoAuthenticatedUser); - } - + var userId = _userNameRegistry.GetCurrentUserExternalId(); var dialog = await _dbContext.Dialogs .AsNoTracking() .Include(x => x.SeenLog.Where(x => x.Id == request.SeenLogId)) @@ -84,7 +79,7 @@ public async Task Handle(GetDialogSeenLogQuery request, } var dto = _mapper.Map(seenLog); - dto.IsCurrentEndUser = userPid == seenLog.EndUserId; + dto.IsCurrentEndUser = userId == seenLog.EndUserId; dto.EndUserIdHash = _stringHasher.Hash(seenLog.EndUserId); return dto; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs index 6fedd73ed..b1bfe92d3 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogSeenLogs/Queries/Search/SearchDialogSeenLogQuery.cs @@ -1,7 +1,6 @@ using AutoMapper; using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; -using Digdir.Domain.Dialogporten.Application.Common.Authentication; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; @@ -43,11 +42,7 @@ public SearchDialogSeenLogQueryHandler( public async Task Handle(SearchDialogSeenLogQuery request, CancellationToken cancellationToken) { - if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) - { - return new Forbidden(Constants.NoAuthenticatedUser); - } - + var userId = _userNameRegistry.GetCurrentUserExternalId(); var dialog = await _db.Dialogs .AsNoTracking() .Include(x => x.SeenLog) @@ -80,7 +75,7 @@ public async Task Handle(SearchDialogSeenLogQuery req .Select(x => { var dto = _mapper.Map(x); - dto.IsCurrentEndUser = x.EndUserId == userPid; + dto.IsCurrentEndUser = x.EndUserId == userId; dto.EndUserIdHash = _stringHasher.Hash(x.EndUserId); return dto; }) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index 254e69a72..78f3d00db 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -9,7 +9,6 @@ using MediatR; using Microsoft.EntityFrameworkCore; using OneOf; -using AuthenticationConstants = Digdir.Domain.Dialogporten.Application.Common.Authentication.Constants; namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get; @@ -54,14 +53,7 @@ public GetDialogQueryHandler( public async Task Handle(GetDialogQuery request, CancellationToken cancellationToken) { - var userInformation = await _userNameRegistry.GetUserInformation(cancellationToken); - - if (userInformation is null) - { - return new Forbidden(AuthenticationConstants.NoAuthenticatedUser); - } - - var (userPid, userName) = userInformation; + var (userId, userName) = await _userNameRegistry.GetCurrentUserInformation(cancellationToken); // This query could be written without all the includes as ProjectTo will do the job for us. // However, we need to guarantee an order for sub resources of the dialog aggregate. @@ -108,7 +100,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo // TODO: What if name lookup fails // https://github.com/digdir/dialogporten/issues/387 - dialog.UpdateSeenAt(userPid, userName); + dialog.UpdateSeenAt(userId, userName); var saveResult = await _unitOfWork .WithoutAuditableSideEffects() @@ -125,7 +117,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo .Select(log => { var logDto = _mapper.Map(log); - logDto.IsCurrentEndUser = log.EndUserId == userPid; + logDto.IsCurrentEndUser = log.EndUserId == userId; logDto.EndUserIdHash = _stringHasher.Hash(log.EndUserId); return logDto; }) diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs index c3bfa6ea5..860f10261 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -1,7 +1,6 @@ using AutoMapper; using AutoMapper.QueryableExtensions; using Digdir.Domain.Dialogporten.Application.Common; -using Digdir.Domain.Dialogporten.Application.Common.Authentication; using Digdir.Domain.Dialogporten.Application.Common.Extensions; using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables; using Digdir.Domain.Dialogporten.Application.Common.Pagination; @@ -137,11 +136,7 @@ public SearchDialogQueryHandler( public async Task Handle(SearchDialogQuery request, CancellationToken cancellationToken) { - if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid)) - { - return new Forbidden(Constants.NoAuthenticatedUser); - } - + var userId = _userNameRegistry.GetCurrentUserExternalId(); var searchExpression = Expressions.LocalizedSearchExpression(request.Search, request.SearchCultureCode); var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch( request.Party ?? [], @@ -181,7 +176,7 @@ public async Task Handle(SearchDialogQuery request, Cancella foreach (var seenLog in paginatedList.Items.SelectMany(x => x.SeenSinceLastUpdate)) { // Before we hash the end user id, check if the seen log entry is for the current user - seenLog.IsCurrentEndUser = userPid == seenLog.EndUserIdHash; + seenLog.IsCurrentEndUser = userId == seenLog.EndUserIdHash; // TODO: Add test to not expose un-hashed end user id to the client // https://github.com/digdir/dialogporten/issues/596 seenLog.EndUserIdHash = _stringHasher.Hash(seenLog.EndUserIdHash); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs new file mode 100644 index 000000000..2e65c031a --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs @@ -0,0 +1,45 @@ +using System.Net; +using Azure; +using Digdir.Domain.Dialogporten.Application.Common.Extensions; +using Digdir.Domain.Dialogporten.Application.Externals.Authentication; +using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; +using FluentValidation.Results; + +namespace Digdir.Domain.Dialogporten.WebApi.Common.Authentication; + +public class UserTypeValidationMiddleware +{ + private readonly RequestDelegate _next; + + public UserTypeValidationMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.User.Identity is { IsAuthenticated: true }) + { + var userType = context.User.GetUserType(); + if (userType == UserType.Unknown) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + await context.Response.WriteAsJsonAsync(context.ResponseBuilder( + context.Response.StatusCode, + new List() { new("UserType", + "The request was authenticated, but we were unable to determine valid user type (person, enterprise or system user) in order to authorize the request.") } + )); + + return; + } + } + + await _next(context); + } +} + +public static class UserTypeValidationMiddlewareExtensions +{ + public static IApplicationBuilder UseUserTypeValidation(this IApplicationBuilder app) + => app.UseMiddleware(); +} diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs index e7aa61cda..55671c8ca 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs @@ -26,4 +26,3 @@ internal static class SwaggerSummary internal const string OptimisticConcurrencyNote = "Optimistic concurrency control is implemented using the If-Match header. Supply the Revision value from the GetDialog endpoint to ensure that the dialog is not modified/deleted by another request in the meantime."; } } - diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs index 3af8fb03f..7d45c2fb4 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs @@ -147,6 +147,7 @@ static void BuildAndRun(string[] args) .UseJwtSchemeSelector() .UseAuthentication() .UseAuthorization() + .UseUserTypeValidation() .UseAzureConfiguration() .UseFastEndpoints(x => {