Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using EstateManagementUI.BlazorServer.Authentication;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using Shouldly;

namespace EstateManagementUI.BlazorServer.Tests.Authentication;

public sealed class OidcUserAccessTokenProviderTests
{
[Fact]
public async Task GetAccessTokenAsync_ReturnsAccessTokenFromAuthenticatedUserSession()
{
var principal = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(OidcUserAccessTokenProvider.AccessTokenClaimType, "user-access-token")
}, "TestAuthentication"));
var provider = new OidcUserAccessTokenProvider(
new StubAuthenticationStateProvider(principal));

var result = await provider.GetAccessTokenAsync(CancellationToken.None);

result.IsSuccess.ShouldBeTrue();
result.Data.ShouldBe("user-access-token");
}

[Fact]
public async Task GetAccessTokenAsync_WithoutAuthenticatedSession_ReturnsFailure()
{
var provider = new OidcUserAccessTokenProvider(
new StubAuthenticationStateProvider(new ClaimsPrincipal(new ClaimsIdentity())));

var result = await provider.GetAccessTokenAsync(CancellationToken.None);

result.IsFailed.ShouldBeTrue();
}

private sealed class StubAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ClaimsPrincipal Principal;

public StubAuthenticationStateProvider(ClaimsPrincipal principal)
{
this.Principal = principal;
}

public override Task<AuthenticationState> GetAuthenticationStateAsync()
=> Task.FromResult(new AuthenticationState(this.Principal));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using EstateManagementUI.BusinessLogic.Client;
using Microsoft.AspNetCore.Components.Authorization;
using SimpleResults;

namespace EstateManagementUI.BlazorServer.Authentication;

public sealed class OidcUserAccessTokenProvider : IUserAccessTokenProvider
{
public static string AccessTokenClaimType => "estate_management_access_token";

private readonly AuthenticationStateProvider AuthenticationStateProvider;

public OidcUserAccessTokenProvider(AuthenticationStateProvider authenticationStateProvider)
{
this.AuthenticationStateProvider = authenticationStateProvider;
}

public async Task<Result<string>> GetAccessTokenAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var authenticationState = await this.AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authenticationState.User.Identity?.IsAuthenticated != true)
return Result.Failure("The current user is not authenticated.");

var accessToken = authenticationState.User.FindFirst(AccessTokenClaimType)?.Value;
if (String.IsNullOrWhiteSpace(accessToken))
return Result.Failure("The current user authentication state does not contain an access token.");

return Result.Success<string>(accessToken);
}
}
30 changes: 22 additions & 8 deletions EstateManagementUI.BlazorServer/Common/BoostrapperExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ClientProxyBase;
using EstateManagementUI.BlazorServer.Authentication;
using EstateManagementUI.BlazorServer.Permissions;
using EstateManagementUI.BlazorServer.Testing;
using EstateManagementUI.BlazorServer.TokenManagement;
Expand All @@ -19,6 +20,7 @@
using System.Data.SqlTypes;
using System.Net;
using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using TransactionProcessor.Client;

Expand Down Expand Up @@ -191,6 +193,7 @@ public static WebApplicationBuilder ConfigureLiveAuthentication(this WebApplicat
// Add additional scopes from old app
options.Scope.Add("fileProcessor");
options.Scope.Add("transactionProcessor");
options.Scope.Add("estateReporting");

options.RequireHttpsMetadata = false; // For development - set to true in production

Expand All @@ -209,6 +212,16 @@ public static WebApplicationBuilder ConfigureLiveAuthentication(this WebApplicat
// Handle prompt parameter for forcing re-authentication
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = context =>
{
var accessToken = context.Properties.GetTokenValue("access_token");
if (!String.IsNullOrWhiteSpace(accessToken) && context.Principal?.Identity is ClaimsIdentity identity)
{
identity.AddClaim(new Claim(OidcUserAccessTokenProvider.AccessTokenClaimType, accessToken));
}

return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.IssuerAddress = $"{issuerAddress}/connect/authorize";
Expand Down Expand Up @@ -246,23 +259,24 @@ public static WebApplicationBuilder RegisterPermissionServices(this WebApplicati

public static WebApplicationBuilder RegisterProductionMeriator(this WebApplicationBuilder builder) {
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(EstateRequestHandler).Assembly));
builder.Services.AddSingleton<IApiClient, ApiClient>();
builder.Services.AddScoped<IApiClient, ApiClient>();
builder.Services.AddSingleton<Func<String, String>>(container => (serviceName) => ConfigurationReader.GetBaseServerUri(serviceName).OriginalString);
return builder;
}

public static WebApplicationBuilder RegisterUIServices(this WebApplicationBuilder builder) {
builder.Services.AddSingleton<IEstateUIService, EstateUIService>();
builder.Services.AddSingleton<IOperatorUIService, OperatorUIService>();
builder.Services.AddSingleton<IContractUIService, ContractUIService>();
builder.Services.AddSingleton<IMerchantUIService, MerchantUIService>();
builder.Services.AddSingleton<ICalendarUIService, CalendarUIService>();
builder.Services.AddSingleton<ITransactionUIService, TransactionUIService>();
builder.Services.AddSingleton<IFileProcessingUIService, FileProcessingUIService>();
builder.Services.AddScoped<IEstateUIService, EstateUIService>();
builder.Services.AddScoped<IOperatorUIService, OperatorUIService>();
builder.Services.AddScoped<IContractUIService, ContractUIService>();
builder.Services.AddScoped<IMerchantUIService, MerchantUIService>();
builder.Services.AddScoped<ICalendarUIService, CalendarUIService>();
builder.Services.AddScoped<ITransactionUIService, TransactionUIService>();
builder.Services.AddScoped<IFileProcessingUIService, FileProcessingUIService>();

return builder;
}
public static WebApplicationBuilder RegisterClients(this WebApplicationBuilder builder) {
builder.Services.AddScoped<IUserAccessTokenProvider, OidcUserAccessTokenProvider>();
builder.Services.RegisterHttpClient<IEstateReportingApiClient, EstateReportingApiClient>();
builder.Services.RegisterHttpClient<ISecurityServiceClient, SecurityServiceClient>();
builder.Services.RegisterHttpClient<ITransactionProcessorClient, TransactionProcessorClient>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using EstateManagementUI.BlazorServer.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
Expand Down Expand Up @@ -78,6 +79,7 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Email, $"{roleName.ToLower(CultureInfo.CurrentCulture)}@test.com"),
new Claim("estateId", "11111111-1111-1111-1111-111111111111"),
new Claim(OidcUserAccessTokenProvider.AccessTokenClaimType, "test-user-access-token"),
new Claim(ClaimTypes.Role, roleName),
new Claim("role", roleName)
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace EstateManagementUI.BlazorServer.TokenManagement
{
using EstateManagementUI.BlazorServer.Authentication;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
Expand Down Expand Up @@ -90,6 +91,17 @@ public override async Task ValidatePrincipal(CookieValidatePrincipalContext cont
context.Properties.UpdateTokenValue("access_token", response.AccessToken);
context.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);

if (context.Principal?.Identity is System.Security.Claims.ClaimsIdentity identity)
{
var existingAccessTokenClaim = identity.FindFirst(OidcUserAccessTokenProvider.AccessTokenClaimType);
if (existingAccessTokenClaim is not null)
{
identity.RemoveClaim(existingAccessTokenClaim);
}

identity.AddClaim(new System.Security.Claims.Claim(OidcUserAccessTokenProvider.AccessTokenClaimType, response.AccessToken));
}

DateTime newExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn);
context.Properties.UpdateTokenValue("expires_at", newExpiresAt.ToString("o", CultureInfo.InvariantCulture));

Expand Down
16 changes: 8 additions & 8 deletions EstateManagmentUI.BusinessLogic/Client/ContractMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Task<Result> RemoveTransactionFeeFromProduct(ContractCommands.RemoveTransactionF
public partial class ApiClient : IApiClient {
public async Task<Result<List<ContractModels.RecentContractModel>>> GetRecentContracts(ContractQueries.GetRecentContractsQuery request,
CancellationToken cancellationToken) {
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -52,7 +52,7 @@ public partial class ApiClient : IApiClient {
public async Task<Result<List<ContractModels.ContractDropDownModel>>> GetContracts(ContractQueries.GetContractsForDropDownQuery request,
CancellationToken cancellationToken)
{
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -69,7 +69,7 @@ public partial class ApiClient : IApiClient {
public async Task<Result<List<ContractModels.ContractModel>>> GetContracts(ContractQueries.GetContractsQuery request,
CancellationToken cancellationToken)
{
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -86,7 +86,7 @@ public partial class ApiClient : IApiClient {
public async Task<Result<ContractModels.ContractModel>> GetContract(ContractQueries.GetContractQuery request,
CancellationToken cancellationToken)
{
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -102,7 +102,7 @@ public partial class ApiClient : IApiClient {

public async Task<Result> CreateContract(ContractCommands.CreateContractCommand request,
CancellationToken cancellationToken) {
var token = await this.GetToken(cancellationToken);
var token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -117,7 +117,7 @@ public async Task<Result> CreateContract(ContractCommands.CreateContractCommand

public async Task<Result> AddProductToContract(ContractCommands.AddProductToContractCommand request,
CancellationToken cancellationToken) {
var token = await this.GetToken(cancellationToken);
var token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -132,7 +132,7 @@ public async Task<Result> AddProductToContract(ContractCommands.AddProductToCont

public async Task<Result> AddTransactionFeeToProduct(ContractCommands.AddTransactionFeeToProductCommand request,
CancellationToken cancellationToken) {
var token = await this.GetToken(cancellationToken);
var token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -149,7 +149,7 @@ public async Task<Result> AddTransactionFeeToProduct(ContractCommands.AddTransac
public async Task<Result> RemoveTransactionFeeFromProduct(ContractCommands.RemoveTransactionFeeFromProductCommand request,
CancellationToken cancellationToken)
{
var token = await this.GetToken(cancellationToken);
var token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand Down
16 changes: 6 additions & 10 deletions EstateManagmentUI.BusinessLogic/Client/DateMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,25 @@ Task<Result<List<ComparisonDateModel>>> GetComparisonDates(DateQueries.GetCompar
public partial class ApiClient : IApiClient {
private readonly IEstateReportingApiClient EstateReportingApiClient;
private readonly ISecurityServiceClient SecurityServiceClient;
private readonly IUserAccessTokenProvider UserAccessTokenProvider;
private readonly ITransactionProcessorClient TransactionProcessorClient;
private readonly IFileProcessorClient FileProcessorClient;

public ApiClient(IEstateReportingApiClient estateReportingApiClient,
ISecurityServiceClient securityServiceClient,
IUserAccessTokenProvider userAccessTokenProvider,
ITransactionProcessorClient transactionProcessorClient,
IFileProcessorClient fileProcessorClient) {
this.EstateReportingApiClient = estateReportingApiClient;
this.SecurityServiceClient = securityServiceClient;
this.UserAccessTokenProvider = userAccessTokenProvider;
this.TransactionProcessorClient = transactionProcessorClient;
this.FileProcessorClient = fileProcessorClient;
}
public async Task<Result<List<ComparisonDateModel>>> GetComparisonDates(DateQueries.GetComparisonDatesQuery request,
CancellationToken cancellationToken) {

// Get a token here
var token = await this.GetToken(cancellationToken);
var token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);
Result<List<ComparisonDate>> apiResult = await this.EstateReportingApiClient.GetComparisonDates(token.Data, request.EstateId, cancellationToken);
Expand All @@ -53,14 +55,8 @@ public async Task<Result<List<ComparisonDateModel>>> GetComparisonDates(DateQuer
return Result.Success(comparisonDates);
}

private async Task<Result<String>> GetToken(CancellationToken cancellationToken) {
// Get a token here.
var clientId = ConfigurationReader.GetValueOrDefault("AppSettings", "ClientId", "");
var clientSecret = ConfigurationReader.GetValueOrDefault("AppSettings", "ClientSecret", "");
Result<TokenResponse>? token = await this.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);
return Result.Success<String>(token.Data.AccessToken);
private async Task<Result<String>> GetUserToken(CancellationToken cancellationToken) {
return await this.UserAccessTokenProvider.GetAccessTokenAsync(cancellationToken);
}
}
}
8 changes: 4 additions & 4 deletions EstateManagmentUI.BusinessLogic/Client/EstateMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public partial class ApiClient : IApiClient {
public async Task<Result<EstateModels.EstateModel>> GetEstate(EstateQueries.GetEstateQuery request,
CancellationToken cancellationToken) {
// Get a token here
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -45,7 +45,7 @@ public partial class ApiClient : IApiClient {
public async Task<Result<List<OperatorModels.OperatorModel>>> GetEstateAssignedOperators(EstateQueries.GetAssignedOperatorsQuery request,
CancellationToken cancellationToken) {
// Get a token here
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -61,7 +61,7 @@ public partial class ApiClient : IApiClient {
public async Task<Result> RemoveEstateOperator(EstateCommands.RemoveOperatorFromEstateCommand request,
CancellationToken cancellationToken) {
// Get a token here
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand All @@ -76,7 +76,7 @@ public async Task<Result> AddEstateOperator(EstateCommands.AddOperatorToEstateCo
CancellationToken cancellationToken)
{
// Get a token here
Result<String> token = await this.GetToken(cancellationToken);
Result<String> token = await this.GetUserToken(cancellationToken);
if (token.IsFailed)
return ResultHelpers.CreateFailure(token);

Expand Down
Loading
Loading