Skip to content

Commit

Permalink
Merge pull request #135 from DuendeSoftware/brock/skip_response_handling
Browse files Browse the repository at this point in the history
add a skip response handling metadata flag
  • Loading branch information
leastprivilege authored Sep 25, 2022
2 parents e7ddc85 + b486b71 commit 3e7009d
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using System.Linq;
using Microsoft.AspNetCore.Authentication;

namespace Microsoft.AspNetCore.Builder;

Expand Down Expand Up @@ -53,7 +55,9 @@ public static BffBuilder AddBff(this IServiceCollection services, Action<BffOpti

services.AddSingleton<IPostConfigureOptions<OpenIdConnectOptions>, PostConfigureOidcOptionsForSilentLogin>();

services.AddTransient<IAuthorizationMiddlewareResultHandler, BffAuthorizationMiddlewareResultHandler>();
// wrap ASP.NET Core
services.AddAuthentication();
services.AddTransientDecorator<IAuthenticationService, BffAuthenticationService>();

return new BffBuilder(services);
}
Expand Down
90 changes: 90 additions & 0 deletions src/Duende.Bff/Configuration/Decorator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// Extension methods for the BFF DI services
/// </summary>
static class DecoratorServiceCollectionExtensions
{
internal static void AddTransientDecorator<TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService
{
services.AddDecorator<TService>();
services.AddTransient<TService, TImplementation>();
}

internal static void AddDecorator<TService>(this IServiceCollection services)
{
var registration = services.LastOrDefault(x => x.ServiceType == typeof(TService));
if (registration == null)
{
throw new InvalidOperationException("Service type: " + typeof(TService).Name + " not registered.");
}
if (services.Any(x => x.ServiceType == typeof(Decorator<TService>)))
{
throw new InvalidOperationException("Decorator already registered for type: " + typeof(TService).Name + ".");
}

services.Remove(registration);

if (registration.ImplementationInstance != null)
{
var type = registration.ImplementationInstance.GetType();
var innerType = typeof(Decorator<,>).MakeGenericType(typeof(TService), type);
services.Add(new ServiceDescriptor(typeof(Decorator<TService>), innerType, ServiceLifetime.Transient));
services.Add(new ServiceDescriptor(type, registration.ImplementationInstance));
}
else if (registration.ImplementationFactory != null)
{
services.Add(new ServiceDescriptor(typeof(Decorator<TService>), provider =>
{
return new DisposableDecorator<TService>((TService)registration.ImplementationFactory(provider));
}, registration.Lifetime));
}
else
{
var type = registration.ImplementationType!;
var innerType = typeof(Decorator<,>).MakeGenericType(typeof(TService), type);
services.Add(new ServiceDescriptor(typeof(Decorator<TService>), innerType, ServiceLifetime.Transient));
services.Add(new ServiceDescriptor(type, type, registration.Lifetime));
}
}

}

internal class Decorator<TService>
{
public TService Instance { get; set; }

public Decorator(TService instance)
{
Instance = instance;
}
}

internal class Decorator<TService, TImpl> : Decorator<TService>
where TImpl : class, TService
{
public Decorator(TImpl instance) : base(instance)
{
}
}

internal class DisposableDecorator<TService> : Decorator<TService>, IDisposable
{
public DisposableDecorator(TService instance) : base(instance)
{
}

public void Dispose()
{
(Instance as IDisposable)?.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static IEndpointConventionBuilder AsBffApiEndpoint(this IEndpointConventi
{
return builder.WithMetadata(new BffApiAttribute());
}

/// <summary>
/// Adds marker that will cause the BFF framework to skip all antiforgery for this endpoint.
/// </summary>
Expand All @@ -30,4 +30,14 @@ public static IEndpointConventionBuilder SkipAntiforgery(this IEndpointConventio
{
return builder.WithMetadata(new BffApiSkipAntiforgeryAttribute());
}

/// <summary>
/// Adds marker that will cause the BFF framework will not override the HTTP response status code.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IEndpointConventionBuilder SkipResponseHandling(this IEndpointConventionBuilder builder)
{
return builder.WithMetadata(new BffApiSkipResponseHandlingAttribute());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System;

namespace Duende.Bff;

/// <summary>
/// This attribute indicates that the BFF midleware will not override the HTTP response status code.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class BffApiSkipResponseHandlingAttribute : Attribute, IBffApiSkipResponseHandling
{
}
94 changes: 94 additions & 0 deletions src/Duende.Bff/EndpointProcessing/BffAuthenticationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Builder;

namespace Duende.Bff;

// this decorates the real authentication service to detect when
// Challenge of Forbid is being called for a BFF API endpoint
internal class BffAuthenticationService : IAuthenticationService
{
private readonly IAuthenticationService _inner;
private readonly ILogger<BffAuthenticationService> _logger;

public BffAuthenticationService(
Decorator<IAuthenticationService> decorator,
ILogger<BffAuthenticationService> logger)
{
_inner = decorator.Instance;
_logger = logger;
}

public Task SignInAsync(HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties)
{
return _inner.SignInAsync(context, scheme, principal, properties);
}

public Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProperties? properties)
{
return _inner.SignOutAsync(context, scheme, properties);
}

public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string? scheme)
{
return _inner.AuthenticateAsync(context, scheme);
}

public async Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties)
{
await _inner.ChallengeAsync(context, scheme, properties);

var endpoint = context.GetEndpoint();
if (endpoint != null)
{
if (context.Response.StatusCode == 302)
{
var isBffEndpoint = endpoint.Metadata.GetMetadata<IBffApiEndpoint>() != null;
if (isBffEndpoint)
{
var requireResponseHandling = endpoint.Metadata.GetMetadata<IBffApiSkipResponseHandling>() == null;
if (requireResponseHandling)
{
_logger.LogDebug("Challenge was called for a BFF API endpoint, BFF response handing changing status code to 401.");

context.Response.StatusCode = 401;
context.Response.Headers.Remove("Location");
context.Response.Headers.Remove("Set-Cookie");
}
}
}
}
}

public async Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties)
{
await _inner.ForbidAsync(context, scheme, properties);

var endpoint = context.GetEndpoint();
if (endpoint != null)
{
if (context.Response.StatusCode == 302)
{
var isBffEndpoint = endpoint.Metadata.GetMetadata<IBffApiEndpoint>() != null;
if (isBffEndpoint)
{
var requireResponseHandling = endpoint.Metadata.GetMetadata<IBffApiSkipResponseHandling>() == null;
if (requireResponseHandling)
{
_logger.LogDebug("Forbid was called for a BFF API endpoint, BFF response handing changing status code to 403.");

context.Response.StatusCode = 403;
context.Response.Headers.Remove("Location");
context.Response.Headers.Remove("Set-Cookie");
}
}
}
}
}
}

This file was deleted.

11 changes: 11 additions & 0 deletions src/Duende.Bff/EndpointProcessing/IBffApiSkipResponseHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

namespace Duende.Bff;

/// <summary>
/// Indicates that the BFF midleware will not override the HTTP response status code.
/// </summary>
public interface IBffApiSkipResponseHandling
{
}
21 changes: 17 additions & 4 deletions test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ public async Task forbidden_api_call_should_return_403()
}

[Fact]
public async Task response_status_401_should_return_401()
public async Task challenge_response_should_return_401()
{
await BffHost.BffLoginAsync("alice");
BffHost.LocalApiStatusCodeToReturn = 401;
BffHost.LocalApiResponseStatus = BffHost.ResponseStatus.Challenge;

var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_authz"));
req.Headers.Add("x-csrf", "1");
Expand All @@ -163,10 +163,10 @@ public async Task response_status_401_should_return_401()
}

[Fact]
public async Task response_status_403_should_return_403()
public async Task forbid_response_should_return_403()
{
await BffHost.BffLoginAsync("alice");
BffHost.LocalApiStatusCodeToReturn = 403;
BffHost.LocalApiResponseStatus = BffHost.ResponseStatus.Forbid;

var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_authz"));
req.Headers.Add("x-csrf", "1");
Expand All @@ -175,6 +175,19 @@ public async Task response_status_403_should_return_403()
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}

[Fact]
public async Task challenge_response_when_response_handling_skipped_should_trigger_redirect_for_login()
{
await BffHost.BffLoginAsync("alice");
BffHost.LocalApiResponseStatus = BffHost.ResponseStatus.Challenge;

var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_anon_no_csrf_no_response_handling"));
var response = await BffHost.BrowserClient.SendAsync(req);

response.StatusCode.Should().Be(HttpStatusCode.Redirect);
}


[Fact]
public async Task fallback_policy_should_not_fail()
{
Expand Down
Loading

0 comments on commit 3e7009d

Please sign in to comment.