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
2 changes: 1 addition & 1 deletion EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ private static T Deserialize<T>(string json)
public static (String usename, String password) SqlCredentials = ("sa", "thisisalongpassword123!");

public String GetLocalConnectionString(String databaseName) {
return $"server=localhost,{this.SqlPort};database={databaseName};user id={SqlCredentials.usename};password={SqlCredentials.password};Encrypt=false";
return $"server=127.0.0.1,{this.SqlPort};database={databaseName};user id={SqlCredentials.usename};password={SqlCredentials.password};Encrypt=false";
}

//internal async Task StartSqlContainer(){
Expand Down
12 changes: 12 additions & 0 deletions EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ public class TestAuthHandler : AuthenticationHandler<TestAuthHandlerOptions>
public const string OmitEstateClaim = "Test-Omit-Estate-Claim";
public const string EstateClaim = "Test-Estate-Claim";
public const string EstateClaimType = "Test-Estate-Claim-Type";
public const string ClientType = "Test-Client-Type";
public const string OmitClientTypeClaim = "Test-Omit-Client-Type-Claim";
public const string ClientTypeClaim = "client_type";

public const string AuthenticationScheme = "Test";
private readonly string _defaultUserId;
Expand Down Expand Up @@ -155,6 +158,15 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
claims.Add(new Claim(estateClaimType, estateClaim));
}

if (Context.Request.Headers.ContainsKey(OmitClientTypeClaim) == false)
{
string clientType = Context.Request.Headers.TryGetValue(ClientType, out var requestedClientType)
? requestedClientType.ToString()
: "user";

claims.Add(new Claim(ClientTypeClaim, clientType));
}

// TODO: Add as many claims as you need here

var identity = new ClaimsIdentity(claims, AuthenticationScheme);
Expand Down
48 changes: 47 additions & 1 deletion EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,44 @@ public async Task EstateEndpoint_ValidEstateClaimWithoutHeader_ReturnsEstate()
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}

[Fact]
public async Task EstateEndpoint_MissingClientTypeClaim_ReturnsForbidden()
{
using HttpResponseMessage response = await SendEstateRequest(omitClientTypeClaim: true);

response.StatusCode.ShouldBe(HttpStatusCode.Forbidden);
}

[Fact]
public async Task EstateEndpoint_UserClientType_UsesEstateClaim()
{
await this.helper.AddEstate("Test Estate", "Ref1");

using HttpResponseMessage response = await SendEstateRequest(clientType: "user");

response.StatusCode.ShouldBe(HttpStatusCode.OK);
}

[Fact]
public async Task EstateEndpoint_ClientCredentialsClientType_UsesEstateHeader()
{
await this.helper.AddEstate("Test Estate", "Ref1");

using HttpResponseMessage response = await SendEstateRequest(
estateHeader: this.TestId.ToString(),
clientType: "client");

response.StatusCode.ShouldBe(HttpStatusCode.OK);
}

[Fact]
public async Task EstateEndpoint_ClientCredentialsClientTypeWithoutEstateHeader_ReturnsForbidden()
{
using HttpResponseMessage response = await SendEstateRequest(clientType: "client");

response.StatusCode.ShouldBe(HttpStatusCode.Forbidden);
}

[Fact]
public async Task EstateEndpoint_DifferentEstateHeader_ReturnsForbidden()
{
Expand Down Expand Up @@ -156,7 +194,9 @@ private async Task<HttpResponseMessage> SendEstateRequest(
string? estateHeader = null,
bool omitEstateClaim = false,
string? estateClaim = null,
string? estateClaimType = null)
string? estateClaimType = null,
string? clientType = null,
bool omitClientTypeClaim = false)
{
using HttpRequestMessage request = new(HttpMethod.Get, this.BaseRoute);
request.Headers.Authorization = new AuthenticationHeaderValue("Test");
Expand All @@ -173,6 +213,12 @@ private async Task<HttpResponseMessage> SendEstateRequest(
if (estateClaimType != null)
request.Headers.Add(TestAuthHandler.EstateClaimType, estateClaimType);

if (clientType != null)
request.Headers.Add(TestAuthHandler.ClientType, clientType);

if (omitClientTypeClaim)
request.Headers.Add(TestAuthHandler.OmitClientTypeClaim, "true");

return await this.Client.SendAsync(request);
}

Expand Down
79 changes: 33 additions & 46 deletions EstateReportingAPI/Common/EstateContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using OpenIddict.Abstractions;
using System.Security.Claims;

namespace EstateReportingAPI.Common
Expand All @@ -21,43 +22,36 @@ internal void SetEstate(Guid estateId)
}
}

public sealed class EstateContextMiddleware
{
public sealed class EstateContextMiddleware {
private readonly RequestDelegate _next;

public EstateContextMiddleware(RequestDelegate next)
{
public EstateContextMiddleware(RequestDelegate next) {
_next = next;
}

public async Task InvokeAsync(
HttpContext httpContext,
EstateContext estateContext,
IConfiguration configuration)
{
if (IsAnonymousEndpoint(httpContext))
{
IConfiguration configuration) {
if (IsAnonymousEndpoint(httpContext)) {
await _next(httpContext);
return;
}

bool disableAuthorisation = configuration.GetValue<Boolean>("AppSettings:DisableAuthorisation");
bool isAuthenticated = httpContext.User.Identity?.IsAuthenticated == true;

if (isAuthenticated == false && disableAuthorisation == false)
{
if (isAuthenticated == false && disableAuthorisation == false) {
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}

if (TryGetHeaderEstateId(httpContext, out Guid? headerEstateId) == false)
{
if (TryGetHeaderEstateId(httpContext, out Guid? headerEstateId) == false) {
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}

if (isAuthenticated)
{
if (isAuthenticated) {
await HandleAuthenticatedRequestAsync(httpContext, estateContext, headerEstateId);
return;
}
Expand All @@ -74,27 +68,22 @@ private static bool IsAnonymousEndpoint(HttpContext httpContext) {
|| path.StartsWithSegments("/openapi", StringComparison.OrdinalIgnoreCase);
}

private static bool TryGetHeaderEstateId(HttpContext httpContext, out Guid? estateId)
{
private static bool TryGetHeaderEstateId(HttpContext httpContext, out Guid? estateId) {
estateId = null;

if (httpContext.Request.Headers.TryGetValue("estateId", out var headerValues) == false)
{
if (httpContext.Request.Headers.TryGetValue("estateId", out var headerValues) == false) {
return true;
}

if (headerValues.Count != 1)
{
if (headerValues.Count != 1) {
return false;
}

if (Guid.TryParse(headerValues[0], out Guid parsedEstateId) == false)
{
if (Guid.TryParse(headerValues[0], out Guid parsedEstateId) == false) {
return false;
}

if (parsedEstateId == Guid.Empty)
{
if (parsedEstateId == Guid.Empty) {
return false;
}

Expand All @@ -105,10 +94,8 @@ private static bool TryGetHeaderEstateId(HttpContext httpContext, out Guid? esta
private async Task HandleUnauthenticatedRequestAsync(
HttpContext httpContext,
EstateContext estateContext,
Guid? headerEstateId)
{
if (headerEstateId.HasValue == false)
{
Guid? headerEstateId) {
if (headerEstateId.HasValue == false) {
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
Expand All @@ -117,38 +104,38 @@ private async Task HandleUnauthenticatedRequestAsync(
await _next(httpContext);
}

private async Task HandleAuthenticatedRequestAsync(
HttpContext httpContext,
EstateContext estateContext,
Guid? headerEstateId)
{
bool isUserToken = httpContext.User.HasClaim(claim => claim.Type == ClaimTypes.NameIdentifier);
private async Task HandleAuthenticatedRequestAsync(HttpContext httpContext,
EstateContext estateContext,
Guid? headerEstateId) {
Boolean hasClientTypeClaim = httpContext.User.HasClaim(claim => claim.Type == "client_type");
if (hasClientTypeClaim == false) {
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}

Boolean isUserToken = httpContext.User.GetClaim("client_type") switch {
"user" => true,
_ => false
};

if (isUserToken)
{
if (isUserToken) {
string? estateClaim = httpContext.User.FindFirst("estateId")?.Value;

if (!Guid.TryParse(estateClaim, out Guid authenticatedEstateId) ||
authenticatedEstateId == Guid.Empty)
{
if (!Guid.TryParse(estateClaim, out Guid authenticatedEstateId) || authenticatedEstateId == Guid.Empty) {
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}

if (headerEstateId.HasValue &&
headerEstateId.Value != authenticatedEstateId)
{
if (headerEstateId.HasValue && headerEstateId.Value != authenticatedEstateId) {
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}

estateContext.SetEstate(authenticatedEstateId);
}
else
{
else {
// Client-credentials token: tenant comes from the trusted ACL header.
if (!headerEstateId.HasValue)
{
if (!headerEstateId.HasValue) {
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
Expand Down
Loading