Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
martinothamar committed Nov 29, 2024
1 parent e298d5a commit 3cb8eab
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 45 deletions.
27 changes: 25 additions & 2 deletions src/Altinn.App.Api/Controllers/AuthenticationController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#nullable disable
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Constants;
using Altinn.App.Core.Internal.Auth;
using Altinn.Platform.Profile.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
Expand All @@ -15,16 +15,39 @@ public class AuthenticationController : ControllerBase
{
private readonly IAuthenticationClient _authenticationClient;
private readonly GeneralSettings _settings;
private readonly IAuthenticationContext _authenticationContext;

/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationController"/> class
/// </summary>
public AuthenticationController(IAuthenticationClient authenticationClient, IOptions<GeneralSettings> settings)
public AuthenticationController(
IAuthenticationClient authenticationClient,
IOptions<GeneralSettings> settings,
IAuthenticationContext authenticationContext
)
{
_authenticationClient = authenticationClient;
_settings = settings.Value;
_authenticationContext = authenticationContext;
}

// /// <summary>
// /// Gets current party by reading cookie value and validating.
// /// </summary>
// /// <returns>Party id for selected party. If invalid, partyId for logged in user is returned.</returns>
// [Authorize]
// [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
// [HttpGet("{org}/{app}/api/[controller]/current")]
// public async Task<ActionResult> GetCurrent()
// {
// bool returnPartyObject = false;
// }

// private sealed record CurrentAuthenticationResponse
// {
// public required UserProfile? Profile { get; init; }
// }

/// <summary>
/// Refreshes the AltinnStudioRuntime JwtToken when not in AltinnStudio mode.
/// </summary>
Expand Down
156 changes: 113 additions & 43 deletions src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using System.Globalization;
using System.Net.Security;
using System.Text.Json;
using System.Text.Json.Serialization;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Internal.Profile;
using Altinn.App.Core.Internal.Registers;
using Altinn.Platform.Profile.Models;
using Altinn.Platform.Register.Models;
using AltinnCore.Authentication.Constants;
using AltinnCore.Authentication.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace Altinn.App.Core.Internal.Auth;
Expand All @@ -19,28 +25,44 @@ internal static void AddAuthenticationContext(this IServiceCollection services)
}
}

internal abstract record AuthenticationInfo
public abstract record AuthenticationInfo

Check failure on line 28 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo'

Check failure on line 28 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo'

Check failure on line 28 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo'
{
public string Token { get; }

Check failure on line 30 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Token'

Check failure on line 30 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Token'

Check failure on line 30 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Token'

private AuthenticationInfo(string token) => Token = token;

internal sealed record Unauthenticated(string Token) : AuthenticationInfo(Token);
public sealed record Unauthenticated(string Token) : AuthenticationInfo(Token);

Check failure on line 34 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Unauthenticated'

Check failure on line 34 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Unauthenticated.Unauthenticated(string)'

Check failure on line 34 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Unauthenticated'

Check failure on line 34 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Unauthenticated.Unauthenticated(string)'

Check failure on line 34 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Unauthenticated'

Check failure on line 34 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.Unauthenticated.Unauthenticated(string)'

internal sealed record User(int UserId, int PartyId, string Token) : AuthenticationInfo(Token);
public sealed record User(

Check failure on line 36 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User'

Check failure on line 36 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.User(int, int, Party?, Party?, UserProfile, int, string)'

Check failure on line 36 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User'

Check failure on line 36 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.User(int, int, Party?, Party?, UserProfile, int, string)'

Check failure on line 36 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User'

Check failure on line 36 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.User(int, int, Party?, Party?, UserProfile, int, string)'
int UserId,

Check failure on line 37 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.UserId'

Check failure on line 37 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.UserId'

Check failure on line 37 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.UserId'
int PartyId,

Check failure on line 38 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.PartyId'

Check failure on line 38 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.PartyId'

Check failure on line 38 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.PartyId'
Party? Reportee,

Check failure on line 39 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.Reportee'

Check failure on line 39 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.Reportee'

Check failure on line 39 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.Reportee'
Party? Party,

Check failure on line 40 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.Party'

Check failure on line 40 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.Party'

Check failure on line 40 in src/Altinn.App.Core/Internal/Auth/IAuthenticationContext.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'AuthenticationInfo.User.Party'
UserProfile Profile,
int AuthenticationLevel,
string Token
) : AuthenticationInfo(Token);

internal sealed record ServiceOwner(string OrgName, string OrgNo, string Token) : AuthenticationInfo(Token);
public sealed record ServiceOwner(string OrgName, string OrgNo, int AuthenticationLevel, string Token)
: AuthenticationInfo(Token);

internal sealed record Org(string OrgNo, int PartyId, string Token) : AuthenticationInfo(Token);
public sealed record Org(string OrgNo, int PartyId, int AuthenticationLevel, string Token)
: AuthenticationInfo(Token);

internal sealed record SystemUser(IReadOnlyList<string> SystemUserId, string SystemId, string Token)
public sealed record SystemUser(IReadOnlyList<string> SystemUserId, string SystemId, string Token)
: AuthenticationInfo(Token);

// internal sealed record App(string Token) : ClientContextData;
// public sealed record App(string Token) : ClientContextData;

internal static AuthenticationInfo From(HttpContext httpContext, string cookieName)
internal static async Task<AuthenticationInfo> From(
HttpContext httpContext,
string authCookieName,
string partyCookieName,
Func<int, Task<UserProfile?>> getUserProfile,
Func<int, Task<Party?>> lookupParty
)
{
string token = JwtTokenUtil.GetTokenFromContext(httpContext, cookieName);
string token = JwtTokenUtil.GetTokenFromContext(httpContext, authCookieName);
if (string.IsNullOrWhiteSpace(token))
throw new InvalidOperationException("Couldn't extract current client token from context");

Expand All @@ -64,22 +86,40 @@ internal static AuthenticationInfo From(HttpContext httpContext, string cookieNa
claim.Type.Equals(AltinnCoreClaimTypes.OrgNumber, StringComparison.OrdinalIgnoreCase)
);

var authLevelClaim = httpContext.User.Claims.FirstOrDefault(claim =>
claim.Type.Equals(AltinnCoreClaimTypes.AuthenticationLevel, StringComparison.OrdinalIgnoreCase)
);

int authLevel = -1;
static void ParseAuthLevel(string? value, out int authLevel)
{
if (!int.TryParse(value, CultureInfo.InvariantCulture, out authLevel))
throw new InvalidOperationException("Missing authentication level claim value for token");

if (authLevel > 4 || authLevel < 0) // TODO - better validation?
throw new InvalidOperationException("Invalid authentication level claim value for token");
}

if (!string.IsNullOrWhiteSpace(orgClaim?.Value))
{
// In this case the token should have a serviceowner scope,
// due to the `urn:altinn:org` claim
if (string.IsNullOrWhiteSpace(orgNoClaim?.Value))
throw new InvalidOperationException("Missing org number claim for org token");
throw new InvalidOperationException("Missing org number claim for service owner token");
if (!string.IsNullOrWhiteSpace(partyIdClaim?.Value))
throw new InvalidOperationException("Got service owner token");

ParseAuthLevel(authLevelClaim?.Value, out authLevel);

// TODO: check if the org is the same as the owner of the app? A flag?

return new ServiceOwner(orgClaim.Value, orgNoClaim.Value, token);
return new ServiceOwner(orgClaim.Value, orgNoClaim.Value, authLevel, token);
}
else if (!string.IsNullOrWhiteSpace(orgNoClaim?.Value))
{
return new Org(orgNoClaim.Value, partyId, token);
ParseAuthLevel(authLevelClaim?.Value, out authLevel);

return new Org(orgNoClaim.Value, partyId, authLevel, token);
}

var authorizationDetailsClaim = httpContext.User.Claims.FirstOrDefault(claim =>
Expand Down Expand Up @@ -118,7 +158,22 @@ internal static AuthenticationInfo From(HttpContext httpContext, string cookieNa
if (!int.TryParse(userIdClaim.Value, CultureInfo.InvariantCulture, out int userId))
throw new InvalidOperationException("Invalid user ID claim value for user token");

return new User(userId, partyId, token);
var userProfile =
await getUserProfile(userId)
?? throw new InvalidOperationException("Could not get user profile while getting user context");

if (httpContext.Request.Cookies.TryGetValue(partyCookieName, out var partyCookie) && partyCookie != null)
{
if (!int.TryParse(partyCookie, CultureInfo.InvariantCulture, out var cookiePartyId))
throw new InvalidOperationException("Invalid party ID in cookie: " + partyCookie);

partyId = cookiePartyId;
}

ParseAuthLevel(authLevelClaim?.Value, out authLevel);

var reportee = partyId == userProfile.PartyId ? userProfile.Party : await lookupParty(partyId);
return new User(userId, partyId, reportee, userProfile.Party, userProfile, authLevel, token);
}

private sealed record AuthorizationDetailsClaim([property: JsonPropertyName("type")] string Type);
Expand All @@ -130,53 +185,68 @@ private sealed record SystemUserAuthorizationDetailsClaim(
);
}

internal interface IAuthenticationContext
public interface IAuthenticationContext
{
AuthenticationInfo Current { get; }
}

internal sealed class AuthenticationContext : IAuthenticationContext
{
private const string ItemsKey = "Internal_AltinnAuthenticationInfo";
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IOptionsMonitor<AppSettings> _appSettings;

private readonly object _lck = new();

public AuthenticationContext(IHttpContextAccessor httpContextAccessor, IOptionsMonitor<AppSettings> appSettings)
private readonly IOptionsMonitor<GeneralSettings> _generalSettings;
private readonly IProfileClient _profileClient;
private readonly IAltinnPartyClient _altinnPartyClient;

public AuthenticationContext(
IHttpContextAccessor httpContextAccessor,
IOptionsMonitor<AppSettings> appSettings,
IOptionsMonitor<GeneralSettings> generalSettings,
IProfileClient profileClient,
IAltinnPartyClient altinnPartyClient
)
{
_httpContextAccessor = httpContextAccessor;
_appSettings = appSettings;
_generalSettings = generalSettings;
_profileClient = profileClient;
_altinnPartyClient = altinnPartyClient;
}

// Currently we're coupling this to the HTTP context directly.
// In the future we might want to run work (e.g. service tasks) in the background,
// at which point we won't always have a HTTP context available.
// At that point we probably want to implement something like an `IExecutionContext`, `IExecutionContextAccessor`
// to decouple ourselves from the ASP.NET request context.
// TODO: consider removing dependcy on HTTP context
private HttpContext _httpContext =>
_httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No HTTP context available");

internal async Task ResolveCurrent()
{
var httpContext = _httpContext;
var authInfo = await AuthenticationInfo.From(
httpContext,
_appSettings.CurrentValue.RuntimeCookieName,
_generalSettings.CurrentValue.GetAltinnPartyCookieName,
_profileClient.GetUserProfile,
_altinnPartyClient.GetParty
);
httpContext.Items[ItemsKey] = authInfo;
}

public AuthenticationInfo Current
{
get
{
// Currently we're coupling this to the HTTP context directly.
// In the future we might want to run work (e.g. service tasks) in the background,
// at which point we won't always have a HTTP context available.
// At that point we probably want to implement something like an `IExecutionContext`, `IExecutionContextAccessor`
// to decouple ourselves from the ASP.NET request context.
// TODO: consider removing dependcy on HTTP context
var httpContext =
_httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No HTTP context available");

lock (_lck)
{
const string key = "Internal_AltinnAuthenticationInfo";
if (httpContext.Items.TryGetValue(key, out var authInfoObj))
{
if (authInfoObj is not AuthenticationInfo authInfo)
throw new InvalidOperationException("Invalid authentication info object in HTTP context items");
return authInfo;
}
else
{
var authInfo = AuthenticationInfo.From(httpContext, _appSettings.CurrentValue.RuntimeCookieName);
httpContext.Items[key] = authInfo;
return authInfo;
}
}
var httpContext = _httpContext;

if (httpContext.Items.TryGetValue(ItemsKey, out var authInfoObj))
throw new InvalidOperationException("Authentication info was not populated");
if (authInfoObj is not AuthenticationInfo authInfo)
throw new InvalidOperationException("Invalid authentication info object in HTTP context items");
return authInfo;
}
}
}

0 comments on commit 3cb8eab

Please sign in to comment.