Skip to content

Commit

Permalink
Added support for signalr negotiation directly from claims
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesRandall committed Feb 20, 2020
1 parent 7a03524 commit 45f4b7e
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<CommonPackageVersion>4.0.52-beta.4</CommonPackageVersion>
<CommonPackageVersion>4.0.56-beta.4</CommonPackageVersion>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,16 @@ public interface ISignalRFunctionBuilder
/// <param name="hubName">The hub name</param>
/// <returns></returns>
ISignalRFunctionBuilder Negotiate(string route, string hubName, string userIdExpression = null, AuthorizationTypeEnum? authorizationType = null, params HttpMethod[] method);

/// <summary>
/// Creates a SignalR negotiator at the specified route attached to the given hub name that will source the user ID from the specified claim.
/// Claim based negotiators always use token based authorization.
/// </summary>
/// <param name="route">The route for the negotiator</param>
/// <param name="hubName">The name of the hub</param>
/// <param name="claimType">The type of claim to use for a user ID</param>
/// <param name="method">The HTTP methods to bind to</param>
/// <returns></returns>
ISignalRFunctionBuilder NegotiateWithClaim(string route, string hubName, string claimType, params HttpMethod[] method);
}
}
3 changes: 2 additions & 1 deletion Source/FunctionMonkey.Compiler.Core/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ private bool ValidateCommandTypes(FunctionHostBuilder builder)
// SignalRBindingExpressionNegotiateCommand is a type used only to make a non dispatching HTTP function
// definition work, it doesn't get used with any mediator and is defined within Function Monkey. It is
// exempt from mediator type checking
if (functionDefinition.CommandType != typeof(SignalRBindingExpressionNegotiateCommand))
if (functionDefinition.CommandType != typeof(SignalRBindingExpressionNegotiateCommand) &&
functionDefinition.CommandType != typeof(SignalRClaimTypeNegotiateCommand))
{
if (!typeSafetyEnforcer.IsValidType(functionDefinition.CommandType))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@
<EmbeddedResource Include="Templates\AzureFunctions\signalr.outputparameter.csharp.handlebars" />
<EmbeddedResource Include="Templates\AzureFunctions\signalrbindingexpressionnegotiate.csharp.handlebars" />
<EmbeddedResource Include="Templates\AzureFunctions\signalrbindingexpressionnegotiate.json.handlebars" />
<EmbeddedResource Include="Templates\AzureFunctions\signalrclaimnegotiate.csharp.handlebars" />
<EmbeddedResource Include="Templates\AzureFunctions\signalrclaimnegotiate.json.handlebars" />
<EmbeddedResource Include="Templates\AzureFunctions\signalrcommandnegotiate.csharp.handlebars" />
<EmbeddedResource Include="Templates\AzureFunctions\signalrcommandnegotiate.json.handlebars" />
<EmbeddedResource Include="Templates\AzureFunctions\startup.csharp.handlebars" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class TemplateProvider : ITemplateProvider
{typeof(CosmosDbFunctionDefinition), "cosmosdb" },
{typeof(SignalRCommandNegotiateFunctionDefinition), "signalrcommandnegotiate" },
{typeof(SignalRBindingExpressionNegotiateFunctionDefinition), "signalrbindingexpressionnegotiate" },
{typeof(SignalRClaimNegotiateFunctionDefinition), "signalrclaimnegotiate" },
{typeof(EventHubFunctionDefinition),"eventhub" },
// output bindings
{typeof(ServiceBusQueueOutputBinding), "servicebusqueue" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal class TriggerReferenceProvider : ITriggerReferenceProvider
{typeof(CosmosDbFunctionDefinition), typeof(CosmosDBTriggerAttribute).Assembly },
{typeof(SignalRCommandNegotiateFunctionDefinition), typeof(SignalRAttribute).Assembly },
{typeof(SignalRBindingExpressionNegotiateFunctionDefinition), typeof(SignalRAttribute).Assembly },
{typeof(SignalRClaimNegotiateFunctionDefinition), typeof(SignalRAttribute).Assembly },
{typeof(EventHubFunctionDefinition), typeof(EventHubAttribute).Assembly}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using FunctionMonkey.Abstractions.Builders.Model;
using System.Security.Claims;
using FunctionMonkey.SignalR;

namespace {{Namespace}}
{
public class {{Name}}
{
public {{Name}}(IServiceProvider serviceProvider)
{
FunctionMonkey.Runtime.FunctionServiceProvider.Value = serviceProvider;
}

[FunctionName("{{Name}}")]
public async Task<IActionResult> Run(
[HttpTrigger(
{{{azureAuthenticationType}}},
{{{httpVerbs}}},
Route = "{{Route}}")
]
HttpRequest req,
ILogger log,
ExecutionContext executionContext
)
{
log.LogInformation("HTTP trigger function {{Name}} processed a request.");
FunctionMonkey.PluginFunctions pluginFunctions = FunctionMonkey.Runtime.PluginFunctions["{{Name}}"];

FunctionMonkey.Runtime.FunctionProvidedLogger.Value = log;

string requestUrl = GetRequestUrl(req);
var contextSetter = (FunctionMonkey.Abstractions.IContextSetter)
FunctionMonkey.Runtime.ServiceProvider.GetService(typeof(FunctionMonkey.Abstractions.IContextSetter));
contextSetter.SetExecutionContext(executionContext.FunctionDirectory,
executionContext.FunctionAppDirectory,
executionContext.FunctionName,
executionContext.InvocationId);
var headerDictionary = new Dictionary<string, IReadOnlyCollection<string>>();
foreach (var headerKeyValuesPair in req.Headers)
{
string[] values = headerKeyValuesPair.Value.ToArray();
headerDictionary.Add(headerKeyValuesPair.Key, values);
}
contextSetter.SetHttpContext(null, requestUrl, headerDictionary);

System.Security.Claims.ClaimsPrincipal principal = null;

// Claim based negotiatiors always validate tokens

if (req.Headers["{{TokenHeader}}"].Count == 0)
{
return new UnauthorizedResult();
}
string authorizationHeader = req.Headers["{{TokenHeader}}"][0];
if (string.IsNullOrWhiteSpace(authorizationHeader))
{
return new UnauthorizedResult();
}

principal = await pluginFunctions.ValidateToken(authorizationHeader);
if (principal == null)
{
return new UnauthorizedResult();
}
contextSetter.SetHttpContext(principal, requestUrl, headerDictionary);


{{#if AuthorizesClaims}}
var claimsPrincipalAuthorizationResult = await pluginFunctions.IsAuthorized(principal, req.Method, requestUrl);
if (!claimsPrincipalAuthorizationResult)
{
return new UnauthorizedResult();
}
{{/if}}

string userId = principal.FindFirst(claim => claim.Type == "{{{ClaimType}}}").Value;
return CreateSignalRResponse(userId);
}

public static IActionResult CreateSignalRResponse(string userId)
{
List<Claim> claims = new List<Claim>();
claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", userId));

AzureSignalRAuthClient client = new AzureSignalRAuthClient(System.Environment.GetEnvironmentVariable("{{ConnectionStringSettingName}}"));
SignalRConnectionInfo info = client.GetClientConnectionInfo("{{{HubName}}}", claims);
return new OkObjectResult(info);
}

private static string GetRequestUrl(HttpRequest request)
{
string str1 = request.Host.Value;
string str2 = request.PathBase.Value;
string str3 = request.Path.Value;
string str4 = request.QueryString.Value;
return new System.Text.StringBuilder(request.Scheme.Length + "://".Length + str1.Length + str2.Length + str3.Length + str4.Length).Append(request.Scheme).Append("://").Append(str1).Append(str2).Append(str3).Append(str4).ToString();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"generatedBy": "Microsoft.NET.Sdk.Functions.Generator-1.0.26",
"configurationSource": "attributes",
"bindings": [
{
"type": "httpTrigger",
"methods": [
{{{lowerHttpVerbs}}}
],
"route": "{{Route}}",
"authLevel": "{{{jsonAuthenticationType}}}",
"name": "req"
}
{{{outputTriggerJson}}}
],
"disabled": false,
"scriptFile": "../bin/{{AssemblyName}}",
"entryPoint": "{{FunctionClassTypeName}}.Run"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>FunctionMonkey.Compiler</id>
<version>4.0.52-beta.4</version>
<version>4.0.56-beta.4</version>
<authors>James Randall</authors>
<description>Generates Azure Functions from command registrations</description>
<licenseUrl>https://raw.githubusercontent.com/JamesRandall/FunctionMonkey/master/LICENSE</licenseUrl>
Expand Down
2 changes: 1 addition & 1 deletion Source/FunctionMonkey.Compiler/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ static void Main(string[] args)
}
catch (Exception e)
{
compilerLog.Error($"Unexpected error: {e.Message}");
compilerLog.Error($"Unexpected error: {e.Message}\n{e.StackTrace}");
}
}
compilerLog.Message("Compilation complete");
Expand Down
21 changes: 21 additions & 0 deletions Source/FunctionMonkey/Builders/SignalRFunctionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,26 @@ public ISignalRFunctionBuilder Negotiate(string route, string hubName, string us
_definitions.Add(definition);
return new SignalRFunctionConfigurationBuilder<SignalRBindingExpressionNegotiateCommand>(_connectionStringSettingNames, this, definition);
}

public ISignalRFunctionBuilder NegotiateWithClaim(string route, string hubName, string claimType, params HttpMethod[] method)
{
SignalRClaimNegotiateFunctionDefinition definition =
new SignalRClaimNegotiateFunctionDefinition()
{
ConnectionStringSettingName = _connectionStringSettingName,
SubRoute = route,
RouteConfiguration = new HttpRouteConfiguration
{
Route = route
},
Route = route,
Verbs = new HashSet<HttpMethod>(method),
Authorization = AuthorizationTypeEnum.TokenValidation,
HubName = hubName,
ClaimType = claimType
};
_definitions.Add(definition);
return new SignalRFunctionConfigurationBuilder<SignalRBindingExpressionNegotiateCommand>(_connectionStringSettingNames, this, definition);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,10 @@ public ISignalRFunctionBuilder Negotiate(string route, string hubName, string us
{
return _httpFunctionBuilder.Negotiate(route, hubName, userIdMapping, authorizationType, method);
}

public ISignalRFunctionBuilder NegotiateWithClaim(string route, string hubName, string claimType, params HttpMethod[] method)
{
return _httpFunctionBuilder.NegotiateWithClaim(route, hubName, claimType, method);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using AzureFromTheTrenches.Commanding.Abstractions;

namespace FunctionMonkey.Model
{
public class SignalRClaimTypeNegotiateCommand { }

public class SignalRClaimNegotiateFunctionDefinition : HttpFunctionDefinition
{
public SignalRClaimNegotiateFunctionDefinition() : base(typeof(SignalRClaimTypeNegotiateCommand))
{

}

public string ConnectionStringSettingName { get; set; }

public string HubName { get; set; }

public string ClaimType { get; set; }
}
}

0 comments on commit 45f4b7e

Please sign in to comment.