Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ODS-6185] 401, 404, 405 status codes do not return problem details response (RFC 9457) #959

Merged
merged 6 commits into from
Feb 15, 2024
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
Expand Up @@ -464,6 +464,11 @@ void RegisterMiddleware()
.AsSelf()
.SingleInstance();

builder.RegisterType<ComplementErrorDetailsMiddleware>()
.As<IMiddleware>()
.AsSelf()
.SingleInstance();

builder.RegisterType<OAuthContentTypeValidationMiddleware>()
.As<IMiddleware>()
.AsSelf()
Expand Down Expand Up @@ -492,6 +497,9 @@ void RegisterMiddleware()
.SingleInstance();

builder.RegisterType<ErrorTranslator>().SingleInstance();

builder.RegisterType<AuthenticateResultTranslator>().SingleInstance();

builder.RegisterType<ModelStateKeyConverter>().EnableClassInterceptors().SingleInstance();

builder.RegisterType<CachingInterceptor>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.ProblemDetails;
using Microsoft.AspNetCore.Authentication;

namespace EdFi.Ods.Api.ExceptionHandling;

public class AuthenticateResultTranslator
{
private readonly IEdFiProblemDetailsProvider _problemDetailsProvider;

public AuthenticateResultTranslator(IEdFiProblemDetailsProvider problemDetailsProvider)
{
_problemDetailsProvider = problemDetailsProvider;
}

public IEdFiProblemDetails GetProblemDetails(AuthenticateResult result)
{
if (result.None)
{
return new SecurityAuthenticationException(
SecurityAuthenticationException.DefaultDetail,
"Authorization header is missing.");
}

return new SecurityAuthenticationException(
SecurityAuthenticationException.DefaultDetail,
result.Failure.Message,
result.Failure);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public static IApplicationBuilder UseTenantIdentification(this IApplicationBuild
public static IApplicationBuilder UseRequestCorrelation(this IApplicationBuilder builder)
=> builder.UseMiddleware<RequestCorrelationMiddleware>();

public static IApplicationBuilder UseComplementErrorDetails(this IApplicationBuilder builder)
=> builder.UseMiddleware<ComplementErrorDetailsMiddleware>();

/// <summary>
/// Adds the <see cref="EdFiApiAuthenticationMiddleware"/> to the specified <see cref="IApplicationBuilder"/>, which enables authentication capabilities.
/// </summary>
Expand Down
11 changes: 7 additions & 4 deletions Application/EdFi.Ods.Api/Helpers/ControllerHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// See the LICENSE and NOTICES files in the project root for more information.

using System.Net;
using EdFi.Ods.Common.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace EdFi.Ods.Api.Helpers
Expand All @@ -14,11 +15,13 @@ public static class ControllerHelpers
/// Gets an IActionResult returning a 404 Not Found status with no response body.
/// </summary>
/// <returns></returns>
public static IActionResult NotFound()
{
return new ObjectResult(null)
public static IActionResult NotFound(string error)
{
var problemDetails = new NotFoundException(error);

return new ObjectResult(problemDetails)
{
StatusCode = (int) HttpStatusCode.NotFound,
StatusCode = problemDetails.Status,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using System.Threading.Tasks;
using EdFi.Ods.Api.Extensions;
using EdFi.Ods.Common.Constants;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Logging;
using log4net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace EdFi.Ods.Api.Middleware;

/// <summary>
/// Implements middleware that inspects the response and complements missing response details.
/// </summary>
public class ComplementErrorDetailsMiddleware : IMiddleware
{
private readonly ILogContextAccessor _logContextAccessor;

private readonly ILog _logger = LogManager.GetLogger(typeof(ComplementErrorDetailsMiddleware));

public ComplementErrorDetailsMiddleware(
ILogContextAccessor logContextAccessor)
{
_logContextAccessor = logContextAccessor;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await next(context);

if (context.Response is { StatusCode: 404, HasStarted: false })
{
var problemDetails = new NotFoundException(
NotFoundException.DefaultDetail,
$"Path '{context.Request.Path}' does not exist. Check the resource name and try again.");

string correlationId = (string)_logContextAccessor.GetValue(CorrelationConstants.LogContextKey);
problemDetails.CorrelationId = correlationId;

await context.Response.WriteProblemDetailsAsync(problemDetails);

_logger.Error(problemDetails.Message);
}

if (context.Response is { StatusCode: 405, HasStarted: false })
{
var problemDetails = context.Request.Method == "POST" ?
new MethodNotAllowedException(MethodNotAllowedException.DefaultPostDetail) :
new MethodNotAllowedException();

string correlationId = (string)_logContextAccessor.GetValue(CorrelationConstants.LogContextKey);
problemDetails.CorrelationId = correlationId;

await context.Response.WriteProblemDetailsAsync(problemDetails);

_logger.Error(problemDetails.Message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

using System;
using System.Threading.Tasks;
using EdFi.Ods.Api.ExceptionHandling;
using EdFi.Ods.Api.Extensions;
using EdFi.Ods.Common.Constants;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Logging;
using EdFi.Ods.Common.Security;
using log4net;
using Microsoft.AspNetCore.Authentication;
Expand All @@ -16,17 +21,23 @@ namespace EdFi.Ods.Api.Middleware
public class EdFiApiAuthenticationMiddleware
{
private readonly IApiClientContextProvider _apiClientContextProvider;
private readonly AuthenticateResultTranslator _authenticateResultTranslator;
private readonly RequestDelegate _next;
private readonly ILogContextAccessor _logContextAccessor;

public EdFiApiAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes,
IApiClientContextProvider apiClientContextProvider)
public EdFiApiAuthenticationMiddleware(RequestDelegate next,
IAuthenticationSchemeProvider schemes,
IApiClientContextProvider apiClientContextProvider,
AuthenticateResultTranslator authenticateResultTranslator,
ILogContextAccessor logContextAccessor)
{
if (schemes != null)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_apiClientContextProvider = apiClientContextProvider;

_authenticateResultTranslator = authenticateResultTranslator;
Schemes = schemes;
_logContextAccessor = logContextAccessor;
}
else
{
Expand Down Expand Up @@ -71,13 +82,25 @@ public async Task Invoke(HttpContext context)
// NOTE: For our use case we set the api key context into the call context storage. The rest of this code
// is the default implementation and may need to be update on version updates.
// see https://github.com/dotnet/aspnetcore/blob/v3.1.9/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs
var apiClientContext = (ApiClientContext) result.Ticket.Properties.Parameters[nameof(ApiClientContext)];
var apiClientContext = (ApiClientContext)result.Ticket.Properties.Parameters[nameof(ApiClientContext)];
_apiClientContextProvider.SetApiClientContext(apiClientContext);
LogicalThreadContext.Properties[nameof(apiClientContext.ApiClientId)] = apiClientContext.ApiClientId;
}
else if (context.Request.Path.StartsWithSegments("/data") ||
context.Request.Path.StartsWithSegments("/composites") ||
context.Request.Path.StartsWithSegments("/changeQueries"))
{
string correlationId = (string)_logContextAccessor.GetValue(CorrelationConstants.LogContextKey);

var problemDetails = (EdFiProblemDetailsExceptionBase)_authenticateResultTranslator.GetProblemDetails(result);

problemDetails.CorrelationId = correlationId;

await context.Response.WriteProblemDetailsAsync(problemDetails);
}
}

await _next(context);
}
await _next(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
// See the LICENSE and NOTICES files in the project root for more information.

using System;
using System.Configuration;
using System.Net.Http.Headers;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using EdFi.Common.Extensions;
using EdFi.Ods.Api.Providers;
using EdFi.Ods.Common.Exceptions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -56,7 +56,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
if (!authHeader.Scheme.EqualsIgnoreCase(BearerHeaderScheme))
{
_logger.LogDebug(UnknownAuthorizationHeaderScheme);
return AuthenticateResult.NoResult();
return AuthenticateResult.Fail(UnknownAuthorizationHeaderScheme);
}

// If the token value is missing, fail authentication
Expand All @@ -66,11 +66,6 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
return AuthenticateResult.Fail(MissingAuthorizationHeaderBearerTokenValue);
}
}
catch (ConfigurationException)
{
// The Security repository couldn't open a connection to the Security database
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Token authentication failed...");
Expand All @@ -92,5 +87,15 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
return AuthenticateResult.Fail(ex);
}
}

protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
// Base method will set the StatusCode to 401 without checking if the response HasStarted.
// If authentication fails in EdFiApiAuthenticationMiddleware, we dont need to set Status code again.
if (Context.Response.StatusCode == StatusCodes.Status401Unauthorized && Context.Response.HasStarted)
return Task.FromResult(0);

return base.HandleChallengeAsync(properties);
}
}
}
Loading
Loading