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

Rework how we add metadata to skip antiforgery checks #107

Merged
merged 2 commits into from
Jun 27, 2022
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
1 change: 0 additions & 1 deletion samples/JS.Yarp/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Duende.Bff;
using Duende.Bff.Yarp;
using Microsoft.AspNetCore.Builder;
Expand Down
7 changes: 2 additions & 5 deletions src/Duende.Bff.Yarp/BffYarpEndpointRouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,17 @@ public static class BffYarpEndpointRouteBuilderExtensions
/// <param name="endpoints"></param>
/// <param name="localPath"></param>
/// <param name="apiAddress"></param>
/// <param name="requireAntiForgeryCheck"></param>
/// <returns></returns>
public static IEndpointConventionBuilder MapRemoteBffApiEndpoint(
this IEndpointRouteBuilder endpoints,
PathString localPath,
string apiAddress,
bool requireAntiForgeryCheck = true)
string apiAddress)
{
endpoints.CheckLicense();

return endpoints.Map(
localPath.Add("/{**catch-all}").Value!,
RemoteApiEndpoint.Map(localPath, apiAddress))
.WithMetadata(new BffRemoteApiEndpointMetadata { RequireAntiForgeryHeader = requireAntiForgeryCheck });

.WithMetadata(new BffRemoteApiEndpointMetadata());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,36 @@ public static class ReverseProxyEndpointConventionBuilderExtensions
/// </summary>
/// <param name="endpoints"></param>
/// <param name="configureAction"></param>
/// <param name="requireAntiforgeryCheck"></param>
/// <returns></returns>
public static ReverseProxyConventionBuilder MapBffReverseProxy(this IEndpointRouteBuilder endpoints,
Action<IReverseProxyApplicationBuilder> configureAction,
bool requireAntiforgeryCheck = true)
Action<IReverseProxyApplicationBuilder> configureAction)
{
return endpoints.MapReverseProxy(configureAction)
.AsBffApiEndpoint(requireAntiforgeryCheck);
.AsBffApiEndpoint();
}

/// <summary>
/// Adds YARP with anti-forgery protection
/// </summary>
/// <param name="endpoints"></param>
/// <param name="requireAntiforgeryCheck"></param>
/// <returns></returns>
public static ReverseProxyConventionBuilder MapBffReverseProxy(this IEndpointRouteBuilder endpoints,
bool requireAntiforgeryCheck = true)
public static ReverseProxyConventionBuilder MapBffReverseProxy(this IEndpointRouteBuilder endpoints)
{
return endpoints.MapReverseProxy()
.AsBffApiEndpoint(requireAntiforgeryCheck);
.AsBffApiEndpoint();
}


// TODO: do we also need a SkipAntiforgery API?
// TODO: review the API comment below

/// <summary>
/// Adds anti-forgery protection to YARP
/// </summary>
/// <param name="builder"></param>
/// <param name="requireAntiforgeryCheck"></param>
/// <returns></returns>
public static ReverseProxyConventionBuilder AsBffApiEndpoint(this ReverseProxyConventionBuilder builder,
bool requireAntiforgeryCheck = true)
public static ReverseProxyConventionBuilder AsBffApiEndpoint(this ReverseProxyConventionBuilder builder)
{
return builder.WithMetadata(new BffApiAttribute(requireAntiforgeryCheck));
return builder.WithMetadata(new BffApiAttribute());
}

}
23 changes: 4 additions & 19 deletions src/Duende.Bff/BffMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Linq;
using System.Threading.Tasks;
using Duende.Bff.Logging;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -51,25 +50,11 @@ public async Task Invoke(HttpContext context)
return;
}

var localEndpointMetadata = endpoint.Metadata.GetOrderedMetadata<BffApiAttribute>();
if (localEndpointMetadata.Any())
var isBffEndpoint = endpoint.Metadata.GetMetadata<IBffApiEndpoint>() != null;
if (isBffEndpoint)
{
var requireLocalAntiForgeryCheck = localEndpointMetadata.First().RequireAntiForgeryCheck;
if (requireLocalAntiForgeryCheck)
{
if (!context.CheckAntiForgeryHeader(_options))
{
_logger.AntiForgeryValidationFailed(context.Request.Path);

context.Response.StatusCode = 401;
return;
}
}
}
else
{
var remoteEndpoint = endpoint.Metadata.GetMetadata<BffRemoteApiEndpointMetadata>();
if (remoteEndpoint is { RequireAntiForgeryHeader: true })
var requireAntiForgeryCheck = endpoint.Metadata.GetMetadata<IBffApiSkipAntiforgry>() == null;
if (requireAntiForgeryCheck)
{
if (!context.CheckAntiForgeryHeader(_options))
{
Expand Down
21 changes: 4 additions & 17 deletions src/Duende.Bff/Endpoints/BffApiAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,10 @@
namespace Duende.Bff;

/// <summary>
/// Decorates a controller as a local BFF API endpoint
/// This allows the BFF midleware to automatically add the antiforgery header checks as well as 302 to 401 conversion
/// Decorates a controller or action as a local BFF API endpoint
/// By default, this provides anti-forgery protection and response handling.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class BffApiAttribute : Attribute
public class BffApiAttribute : Attribute, IBffApiEndpoint
{
/// <summary>
/// specifies if anti-forgery check is required
/// </summary>
public bool RequireAntiForgeryCheck { get; }

/// <summary>
/// ctor
/// </summary>
/// <param name="requireAntiForgeryCheck">Specifies if the antiforgery header gets checked</param>
public BffApiAttribute(bool requireAntiForgeryCheck = true)
{
RequireAntiForgeryCheck = requireAntiForgeryCheck;
}
}
}
14 changes: 14 additions & 0 deletions src/Duende.Bff/Endpoints/BffApiSkipAntiforgeryAttribute.cs
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 ignore the antiforgery header checks.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class BffApiSkipAntiforgeryAttribute : Attribute, IBffApiSkipAntiforgry
{
}
28 changes: 12 additions & 16 deletions src/Duende.Bff/Endpoints/BffAuthorizationMiddlewareResultHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,21 @@ public BffAuthorizationMiddlewareResultHandler()
var endpoint = context.GetEndpoint();

if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));

var remoteApi = endpoint.Metadata.GetMetadata<BffRemoteApiEndpointMetadata>();
var localApi = endpoint.Metadata.GetMetadata<BffApiAttribute>();

if (remoteApi == null && localApi == null)
var isBffEndpoint = endpoint.Metadata.GetMetadata<IBffApiEndpoint>() != null;
if (isBffEndpoint)
{
return _handler.HandleAsync(next, context, policy, authorizeResult);
}

if (authorizeResult.Challenged)
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}
if (authorizeResult.Challenged)
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}

if (authorizeResult.Forbidden)
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
if (authorizeResult.Forbidden)
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
}
}

return _handler.HandleAsync(next, context, policy, authorizeResult);
Expand Down
7 changes: 1 addition & 6 deletions src/Duende.Bff/Endpoints/BffRemoteApiEndpointMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Duende.Bff;
/// <summary>
/// Endpoint metadata for a remote BFF API endpoint
/// </summary>
public class BffRemoteApiEndpointMetadata
public class BffRemoteApiEndpointMetadata : IBffApiEndpoint
{
/// <summary>
/// Required token type (if any)
Expand All @@ -18,11 +18,6 @@ public class BffRemoteApiEndpointMetadata
/// </summary>
public bool OptionalUserToken { get; set; }

/// <summary>
/// Require presence of anti-forgery header
/// </summary>
public bool RequireAntiForgeryHeader { get; set; } = true;

/// <summary>
/// Maps to UserAccessTokenParameters and included if set
/// </summary>
Expand Down
15 changes: 12 additions & 3 deletions src/Duende.Bff/Endpoints/EndpointConventionBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@ public static class EndpointConventionBuilderExtensions
/// This metadata is used by the BFF middleware.
/// </summary>
/// <param name="builder"></param>
/// <param name="requireAntiForgeryCheck">Specifies if the antiforgery header gets checked</param>
/// <returns></returns>
public static IEndpointConventionBuilder AsBffApiEndpoint(this IEndpointConventionBuilder builder, bool requireAntiForgeryCheck = true)
public static IEndpointConventionBuilder AsBffApiEndpoint(this IEndpointConventionBuilder builder)
{
return builder.WithMetadata(new BffApiAttribute(requireAntiForgeryCheck));
return builder.WithMetadata(new BffApiAttribute());
}

/// <summary>
/// Adds marker that will cause the BFF framework to skip all antiforgery for this endpoint.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IEndpointConventionBuilder SkipAntiforgery(this IEndpointConventionBuilder builder)
{
return builder.WithMetadata(new BffApiSkipAntiforgeryAttribute());
}
}
12 changes: 12 additions & 0 deletions src/Duende.Bff/Endpoints/IBffApiEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

namespace Duende.Bff;

/// <summary>
/// Marks an endpoint as BFF API endpoint.
/// By default, this provides anti-forgery protection and response handling.
/// </summary>
public interface IBffApiEndpoint
{
}
11 changes: 11 additions & 0 deletions src/Duende.Bff/Endpoints/IBffApiSkipAntiforgry.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 ignore the antiforgery header checks.
/// </summary>
public interface IBffApiSkipAntiforgry
{
}
6 changes: 3 additions & 3 deletions test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async Task calls_to_authorized_local_endpoint_should_succeed()
}

[Fact]
public async Task calls_to_authorized_local_endpoint_without_csrf_should_succeed()
public async Task calls_to_authorized_local_endpoint_without_csrf_should_succeed_without_antiforgery_header()
{
await BffHost.BffLoginAsync("alice");

Expand All @@ -61,7 +61,7 @@ public async Task unauthenticated_calls_to_authorized_local_endpoint_should_fail
}

[Fact]
public async Task calls_to_local_endpoint_should_require_csrf()
public async Task calls_to_local_endpoint_should_require_antiforgery_header()
{
var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_anon"));
var response = await BffHost.BrowserClient.SendAsync(req);
Expand All @@ -70,7 +70,7 @@ public async Task calls_to_local_endpoint_should_require_csrf()
}

[Fact]
public async Task calls_to_local_endpoint_without_antiforgery_should_not_require_csrf()
public async Task calls_to_local_endpoint_without_csrf_should_not_require_antiforgery_header()
{
var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_anon_no_csrf"));
var response = await BffHost.BrowserClient.SendAsync(req);
Expand Down
9 changes: 6 additions & 3 deletions test/Duende.Bff.Tests/TestHosts/BffHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ private void Configure(IApplicationBuilder app)
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
})
.AsBffApiEndpoint(requireAntiForgeryCheck: false);
.AsBffApiEndpoint()
.SkipAntiforgery();

endpoints.Map("/local_authz", async context =>
{
Expand Down Expand Up @@ -271,7 +272,8 @@ private void Configure(IApplicationBuilder app)
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
})
.RequireAuthorization()
.AsBffApiEndpoint(requireAntiForgeryCheck: false);
.AsBffApiEndpoint()
.SkipAntiforgery();


endpoints.Map("/always_fail_authz_non_bff_endpoint", context => { return Task.CompletedTask; })
Expand All @@ -286,7 +288,8 @@ private void Configure(IApplicationBuilder app)
.RequireAccessToken();

endpoints.MapRemoteBffApiEndpoint(
"/api_user_no_csrf", _apiHost.Url(), requireAntiForgeryCheck: false)
"/api_user_no_csrf", _apiHost.Url())
.SkipAntiforgery()
.RequireAccessToken();

endpoints.MapRemoteBffApiEndpoint(
Expand Down