Skip to content

Commit

Permalink
Add option to check custom header when receiving http endpoint request (
Browse files Browse the repository at this point in the history
#4230)

* Add option to check custom header when receiving http endpoint request

* Fix grammatic error

* Add header authorization example to sample project

---------

Co-authored-by: Tanel Kuhi <tanel.kuhi@helmes.com>
  • Loading branch information
tanelkuhi and tanelkuhi authored Aug 11, 2023
1 parent 21914db commit 4a6872d
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public class HttpEndpoint : Activity, IActivityPropertyOptionsProvider
/// Allow authenticated requests only
/// </summary>
[ActivityInput(
Hint = "Check to allow authenticated requests only",
Hint = "Check to only allow requests, which satisfy a specified policy",
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
Category = "Security"
)]
Expand All @@ -89,6 +89,25 @@ public class HttpEndpoint : Activity, IActivityPropertyOptionsProvider
Category = "Security"
)]
public string? Policy { get; set; }

[ActivityInput(
Hint = "Check to only allow requests, which have a specified header with a specified value",
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
Category = "Security"
)]
public bool AuthorizeWithCustomHeader { get; set; }

[ActivityInput(
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
Category = "Security"
)]
public string? CustomHeaderName { get; set; }

[ActivityInput(
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
Category = "Security"
)]
public string? CustomHeaderValue { get; set; }

/// <summary>
/// The received HTTP request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,21 @@ public static ISetupActivity<HttpEndpoint> WithMethods(this ISetupActivity<HttpE

/// <inheritdoc cref="WithPolicy(ISetupActivity{HttpEndpoint}, Func{ActivityExecutionContext, ValueTask{string?}})"/>
public static ISetupActivity<HttpEndpoint> WithPolicy(this ISetupActivity<HttpEndpoint> activity, string? value) => activity.Set(x => x.Policy, value);


/// <summary>
/// Sets whether or not this endpoint requires authorization with a custom header to use
/// </summary>
/// <param name="activity"></param>
/// <param name="value">Whether or not authorization with custom header is used.</param>
/// <param name="customHeaderName">The name of the custom header.</param>
/// <param name="customHeaderValue">The value of the custom header</param>
/// <returns>A <see cref="ISetupActivity"/> that can be used to further change properties of <see cref="HttpEndpoint"/></returns>
public static ISetupActivity<HttpEndpoint> WithAuthorizeWithCustomHeader(this ISetupActivity<HttpEndpoint> activity, bool value, string customHeaderName, string customHeaderValue)
{
return activity.Set(x => x.AuthorizeWithCustomHeader, value)
.Set(x => x.CustomHeaderName, customHeaderName)
.Set(x => x.CustomHeaderValue, customHeaderValue);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Options;
using Elsa.Activities.Http.Parsers.Request;
using Elsa.Activities.Http.Services;
using Elsa.Models;
using Elsa.Persistence;
using Elsa.Services;
Expand Down Expand Up @@ -116,7 +117,8 @@ from route in routeTable
var contentParser = orderedContentParsers.FirstOrDefault(x => x.SupportedContentTypes.Contains(simpleContentType, StringComparer.OrdinalIgnoreCase)) ?? orderedContentParsers.LastOrDefault() ?? new DefaultHttpRequestBodyParser();
var activityWrapper = workflowBlueprintWrapper.GetUnfilteredActivity<HttpEndpoint>(pendingWorkflow.ActivityId!)!;

if (!await AuthorizeAsync(httpContext, options.Value, activityWrapper, workflowBlueprint, pendingWorkflow, cancellationToken))
if (!await AuthorizeAsync(httpContext, options.Value, activityWrapper, workflowBlueprint, pendingWorkflow, cancellationToken) ||
!await AuthorizeWithCustomHeaderAsync(httpContext, activityWrapper, workflowBlueprint, pendingWorkflow, cancellationToken))
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
Expand Down Expand Up @@ -215,6 +217,22 @@ private async Task<bool> AuthorizeAsync(

return await authorizationHandler.AuthorizeAsync(new AuthorizeHttpEndpointContext(httpContext, httpEndpoint, workflowBlueprint, pendingWorkflow.WorkflowInstanceId, cancellationToken));
}

private async Task<bool> AuthorizeWithCustomHeaderAsync(
HttpContext httpContext,
IActivityBlueprintWrapper<HttpEndpoint> httpEndpoint,
IWorkflowBlueprint workflowBlueprint,
CollectedWorkflow pendingWorkflow,
CancellationToken cancellationToken)
{
var authorizeWithCustomHeader = await httpEndpoint.EvaluatePropertyValueAsync(x => x.AuthorizeWithCustomHeader, cancellationToken);

if (!authorizeWithCustomHeader)
return true;

var authorizationHandler = ActivatorUtilities.GetServiceOrCreateInstance<CustomHeaderAuthorizationHandler>(httpContext.RequestServices);
return await authorizationHandler.AuthorizeAsync(new AuthorizeHttpEndpointContext(httpContext, httpEndpoint, workflowBlueprint, pendingWorkflow.WorkflowInstanceId, cancellationToken));
}

private async Task<bool> HandleNoWorkflowsFoundAsync(HttpContext httpContext, IList<CollectedWorkflow> pendingWorkflows, PathString? basePath)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Elsa.Activities.Http.Contracts;
using Elsa.Activities.Http.Models;

namespace Elsa.Activities.Http.Services
{
public class CustomHeaderAuthorizationHandler : IHttpEndpointAuthorizationHandler
{
public async ValueTask<bool> AuthorizeAsync(AuthorizeHttpEndpointContext context)
{
var httpContext = context.HttpContext;

var cancellationToken = context.CancellationToken;
var httpEndpoint = context.HttpEndpointActivity;
var headerName = await httpEndpoint.EvaluatePropertyValueAsync(x => x.CustomHeaderName, cancellationToken);
var expectedHeaderValue = await httpEndpoint.EvaluatePropertyValueAsync(x => x.CustomHeaderValue, cancellationToken);

if (string.IsNullOrWhiteSpace(headerName) || string.IsNullOrWhiteSpace(expectedHeaderValue))
return true;

return httpContext.Request.Headers[headerName] == expectedHeaderValue;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ public void Build(IWorkflowBuilder builder)
var user = httpContext.User;
return $"Hello {user.Identity!.Name}!";
}));

builder
.HttpEndpoint(setup => setup
.WithPath("/safe-hello-header")
.WithMethod("GET")
.WithAuthorizeWithCustomHeader(true, "api-key", "test-api-key"))
.WriteHttpResponse(setup => setup.WithStatusCode(HttpStatusCode.OK)
.WithContent(() => "Hello header"));
}
}
}

0 comments on commit 4a6872d

Please sign in to comment.