Skip to content

Commit

Permalink
Add support for identity bindings and samples
Browse files Browse the repository at this point in the history
  • Loading branch information
ConnorMcMahon committed Oct 26, 2018
1 parent f086912 commit 6e34b98
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 7 deletions.
14 changes: 14 additions & 0 deletions WebJobs.Script.sln
Expand Up @@ -275,6 +275,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TimerTrigger", "TimerTrigge
sample\Node\TimerTrigger\index.js = sample\Node\TimerTrigger\index.js
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTrigger-Identities", "HttpTrigger-Identities", "{DCBE49B4-CE2C-4D44-8DD9-2B771343E1C6}"
ProjectSection(SolutionItems) = preProject
sample\CSharp\HttpTrigger-Identities\function.json = sample\CSharp\HttpTrigger-Identities\function.json
sample\CSharp\HttpTrigger-Identities\run.csx = sample\CSharp\HttpTrigger-Identities\run.csx
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTrigger-Identities", "HttpTrigger-Identities", "{909C3F6B-1B36-498F-8707-C6CC54CD2242}"
ProjectSection(SolutionItems) = preProject
sample\Node\HttpTrigger-Identities\function.json = sample\Node\HttpTrigger-Identities\function.json
sample\Node\HttpTrigger-Identities\index.js = sample\Node\HttpTrigger-Identities\index.js
EndProjectSection
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{35c9ccb7-d8b6-4161-bb0d-bcfa7c6dcffb}*SharedItemsImports = 13
Expand Down Expand Up @@ -374,6 +386,8 @@ Global
{1A6107CB-6295-4388-9C63-F21A78D5137E} = {9D87C796-7914-4A43-B843-579562393E10}
{A0AAA922-2AFA-4418-BB85-CE7602394273} = {9D87C796-7914-4A43-B843-579562393E10}
{B773D1BC-495A-45CD-82CF-393A698FD958} = {9D87C796-7914-4A43-B843-579562393E10}
{DCBE49B4-CE2C-4D44-8DD9-2B771343E1C6} = {34506711-9D66-41EF-BBA1-9A9DC1140209}
{909C3F6B-1B36-498F-8707-C6CC54CD2242} = {9D87C796-7914-4A43-B843-579562393E10}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {85400884-5FFD-4C27-A571-58CB3C8CAAC5}
Expand Down
15 changes: 15 additions & 0 deletions sample/CSharp/HttpTrigger-Identities/function.json
@@ -0,0 +1,15 @@
{
"bindings": [
{
"type": "httpTrigger",
"name": "req",
"direction": "in",
"methods": [ "get" ]
},
{
"type": "http",
"name": "$return",
"direction": "out"
}
]
}
32 changes: 32 additions & 0 deletions sample/CSharp/HttpTrigger-Identities/run.csx
@@ -0,0 +1,32 @@
using System.Linq;
using System.Net;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;

public static IActionResult Run(HttpRequest req, TraceWriter log, ClaimsPrincipal principal)
{
log.Info("C# HTTP trigger function processed a request.");

string[] identityStrings = principal.Identities.Select(GetIdentityString).ToArray();

return new OkObjectResult(string.Join(";", identityStrings));
}

private static string GetIdentityString(ClaimsIdentity identity)
{
var userIdClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim != null)
{
// user identity
var userNameClaim = identity.FindFirst(ClaimTypes.Name);
return $"Identity: ({identity.AuthenticationType}, {userNameClaim.Value}, {userIdClaim.Value})";
}
else
{
// key based identity
var authLevelClaim = identity.FindFirst("http://schemas.microsoft.com/2017/07/functions/claims/authlevel");
var keyIdClaim = identity.FindFirst("http://schemas.microsoft.com/2017/07/functions/claims/keyid");
return $"Identity: ({identity.AuthenticationType}, {authLevelClaim.Value}, {keyIdClaim.Value})";
}
}
15 changes: 15 additions & 0 deletions sample/Node/HttpTrigger-Identities/function.json
@@ -0,0 +1,15 @@
{
"bindings": [
{
"type": "httpTrigger",
"name": "req",
"direction": "in",
"methods": [ "get" ]
},
{
"type": "http",
"name": "$return",
"direction": "out"
}
]
}
30 changes: 30 additions & 0 deletions sample/Node/HttpTrigger-Identities/index.js
@@ -0,0 +1,30 @@
module.exports = function (context, req) {
console.dir(context.bindingData.identities);
var identityString = context.bindingData.identities.map(GetIdentityString).join(";");

var res = {
status: 200,
body: identityString,
headers: {
'Content-Type': 'text/plain'
}
};

context.done(null, res);
};

function GetIdentityString(identity) {
var nameIdentifierType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
var userIdClaim = identity.claims.find(claim => claim.type === nameIdentifierType);
if (userIdClaim) {
// user claim
var nameType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name";
var userNameClaim = identity.claims.find(claim => claim.type === nameType);
return `Identity: (${identity.authenticationType}, ${userNameClaim.value}, ${userIdClaim.value})`;
} else {
// key based identity
var authLevelClaim = identity.claims.find(claim => claim.type === "http://schemas.microsoft.com/2017/07/functions/claims/authlevel");
var keyIdClaim = identity.claims.find(claim => claim.type === "http://schemas.microsoft.com/2017/07/functions/claims/keyid");
return `Identity: (${identity.authenticationType}, ${authLevelClaim.value}, ${keyIdClaim.value})`;
}
}
Expand Up @@ -23,17 +23,20 @@ internal class AuthenticationLevelHandler : AuthenticationHandler<Authentication
{
public const string FunctionsKeyHeaderName = "x-functions-key";
private readonly ISecretManagerProvider _secretManagerProvider;
private readonly bool _isEasyAuthEnabled;

public AuthenticationLevelHandler(
IOptionsMonitor<AuthenticationLevelOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IDataProtectionProvider dataProtection,
ISystemClock clock,
ISecretManagerProvider secretManagerProvider)
ISecretManagerProvider secretManagerProvider,
IEnvironment environment)
: base(options, logger, encoder, clock)
{
_secretManagerProvider = secretManagerProvider;
_isEasyAuthEnabled = environment.IsEasyAuthEnabled();
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
Expand All @@ -53,8 +56,19 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
claims.Add(new Claim(SecurityConstants.AuthLevelKeyNameClaimType, name));
}

var identity = new ClaimsIdentity(claims, AuthLevelAuthenticationDefaults.AuthenticationScheme);
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name));
List<ClaimsIdentity> claimsIdentities = new List<ClaimsIdentity>();
var keyIdentity = new ClaimsIdentity(claims, AuthLevelAuthenticationDefaults.AuthenticationScheme);
if (_isEasyAuthEnabled)
{
ClaimsIdentity easyAuthIdentity = Context.Request.GetAppServiceIdentity();
if (easyAuthIdentity != null)
{
claimsIdentities.Add(easyAuthIdentity);
}
}
claimsIdentities.Add(keyIdentity);

return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claimsIdentities), Scheme.Name));
}
else
{
Expand Down
6 changes: 6 additions & 0 deletions src/WebJobs.Script/Environment/EnvironmentExtensions.cs
Expand Up @@ -32,6 +32,12 @@ public static bool IsPlaceholderModeEnabled(this IEnvironment environment)
return environment.GetEnvironmentVariable(AzureWebsitePlaceholderMode) == "1";
}

public static bool IsEasyAuthEnabled(this IEnvironment environment)
{
bool.TryParse(environment.GetEnvironmentVariable(EnvironmentSettingNames.EasyAuthEnabled), out bool isEasyAuthEnabled);
return isEasyAuthEnabled;
}

public static bool IsRunningAsHostedSiteExtension(this IEnvironment environment)
{
if (environment.IsAppServiceEnvironment())
Expand Down
1 change: 1 addition & 0 deletions src/WebJobs.Script/Environment/EnvironmentSettingNames.cs
Expand Up @@ -28,6 +28,7 @@ public static class EnvironmentSettingNames
public const string ConsoleLoggingDisabled = "CONSOLE_LOGGING_DISABLED";
public const string SkipSslValidation = "SCM_SKIP_SSL_VALIDATION";
public const string EnvironmentNameKey = "AZURE_FUNCTIONS_ENVIRONMENT";
public const string EasyAuthEnabled = "WEBSITE_AUTH_ENABLED";

/// <summary>
/// Environment variable dynamically set by the platform when it is safe to
Expand Down
2 changes: 1 addition & 1 deletion src/WebJobs.Script/WebJobs.Script.csproj
Expand Up @@ -39,7 +39,7 @@
<PackageReference Include="Microsoft.Azure.Functions.NodeJsWorker" Version="1.0.0-beta6" />
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.1" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.0-10697" />
<PackageReference Include="Microsoft.Azure.WebJobs.Logging.ApplicationInsights" Version="3.0.2" />
<PackageReference Include="Microsoft.Build" Version="15.8.166" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="2.8.2" />
Expand Down
2 changes: 1 addition & 1 deletion test/TestFunctions/TestFunctions.csproj
Expand Up @@ -17,7 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.0-10697" />
</ItemGroup>

</Project>
Expand Up @@ -293,7 +293,7 @@ public async Task ListFunctions_Succeeds()
var response = await _fixture.Host.HttpClient.SendAsync(request);
var metadata = (await response.Content.ReadAsAsync<IEnumerable<FunctionMetadataResponse>>()).ToArray();

Assert.Equal(15, metadata.Length);
Assert.Equal(16, metadata.Length);
var function = metadata.Single(p => p.Name == "HttpTrigger-CustomRoute");
Assert.Equal("https://localhost/csharp/products/{category:alpha?}/{id:int?}/{extra?}", function.InvokeUrlTemplate.ToString());

Expand Down Expand Up @@ -610,6 +610,55 @@ public async Task HttpTriggerWithObject_Post_Succeeds()
}
}

[Fact]
public async Task HttpTrigger_Identities_Succeeds()
{
var vars = new Dictionary<string, string>
{
{ LanguageWorkerConstants.FunctionWorkerRuntimeSettingName, LanguageWorkerConstants.DotNetLanguageWorkerName},
{ "WEBSITE_AUTH_ENABLED", "TRUE"}
};
using (var env = new TestScopedEnvironmentVariable(vars))
{
string functionKey = await _fixture.Host.GetFunctionSecretAsync("HttpTrigger-Identities");
string uri = $"api/httptrigger-identities?code={functionKey}";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);

MockEasyAuth(request, "facebook", "Connor McMahon", "10241897674253170");

HttpResponseMessage response = await this._fixture.Host.HttpClient.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string responseContent = await response.Content.ReadAsStringAsync();
string[] identityStrings = StripBookendQuotations(responseContent).Split(';');
Assert.Equal("Identity: (facebook, Connor McMahon, 10241897674253170)", identityStrings[0]);
Assert.Equal("Identity: (WebJobsAuthLevel, Function, Key1)", identityStrings[1]);
}
}

[Fact]
public async Task HttpTrigger_Identities_BlocksSpoofedEasyAuthIdentity()
{
var vars = new Dictionary<string, string>
{
{ LanguageWorkerConstants.FunctionWorkerRuntimeSettingName, LanguageWorkerConstants.DotNetLanguageWorkerName},
{ "WEBSITE_AUTH_ENABLED", "FALSE"}
};
using (var env = new TestScopedEnvironmentVariable(vars))
{
string functionKey = await _fixture.Host.GetFunctionSecretAsync("HttpTrigger-Identities");
string uri = $"api/httptrigger-identities?code={functionKey}";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);

MockEasyAuth(request, "facebook", "Connor McMahon", "10241897674253170");

HttpResponseMessage response = await this._fixture.Host.HttpClient.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string responseContent = await response.Content.ReadAsStringAsync();
string identityString = StripBookendQuotations(responseContent);
Assert.Equal("Identity: (WebJobsAuthLevel, Function, Key1)", identityString);
}
}

private async Task<HttpResponseMessage> GetHostStatusAsync()
{
string uri = "admin/host/status";
Expand Down Expand Up @@ -657,6 +706,40 @@ private async Task<HttpResponseMessage> RestartHostAsync()
return await _fixture.Host.HttpClient.SendAsync(request);
}

internal static string StripBookendQuotations(string response)
{
if (response.StartsWith("\"") && response.EndsWith("\""))
{
return response.Substring(1, response.Length - 2);
}
return response;
}

internal static void MockEasyAuth(HttpRequestMessage request, string provider, string name, string id)
{
string userIdentityJson = @"{
""auth_typ"": """ + provider + @""",
""claims"": [
{
""typ"": ""http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"",
""val"": """ + name + @"""
},
{
""typ"": ""http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"",
""val"": """ + name + @"""
},
{
""typ"": ""http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"",
""val"": """ + id + @"""
}
],
""name_typ"": ""http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"",
""role_typ"": ""http://schemas.microsoft.com/ws/2008/06/identity/claims/role""
}";
string easyAuthHeaderValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(userIdentityJson));
request.Headers.Add("x-ms-client-principal", easyAuthHeaderValue);
}

public class TestFixture : EndToEndTestFixture
{
static TestFixture()
Expand All @@ -680,6 +763,7 @@ public override void ConfigureJobHost(IWebJobsBuilder webJobsBuilder)
"HttpTrigger-Compat",
"HttpTrigger-CustomRoute",
"HttpTrigger-POCO",
"HttpTrigger-Identities",
"HttpTriggerWithObject",
"ManualTrigger"
};
Expand Down
Expand Up @@ -449,6 +449,31 @@ public async Task HttpTrigger_Disabled_SucceedsWithAdminKey()
Assert.Equal("Hello World!", body);
}

[Fact]
public async Task HttpTrigger_Identities_Succeeds()
{
var vars = new Dictionary<string, string>
{
{ "WEBSITE_AUTH_ENABLED", "TRUE"}
};
using (var env = new TestScopedEnvironmentVariable(vars))
{
string id = Guid.NewGuid().ToString();
string functionKey = await _fixture.Host.GetFunctionSecretAsync("HttpTrigger-Identities");
string uri = $"api/httptrigger-identities?code={functionKey}";

var request = new HttpRequestMessage(HttpMethod.Get, uri);
SamplesEndToEndTests_CSharp.MockEasyAuth(request, "facebook", "Connor McMahon", "10241897674253170");

HttpResponseMessage response = await this._fixture.Host.HttpClient.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string responseContent = await response.Content.ReadAsStringAsync();
string[] identityStrings = SamplesEndToEndTests_CSharp.StripBookendQuotations(responseContent).Split(';');
Assert.Equal("Identity: (facebook, Connor McMahon, 10241897674253170)", identityStrings[0]);
Assert.Equal("Identity: (WebJobsAuthLevel, Function, Key1)", identityStrings[1]);
}
}

public class TestFixture : EndToEndTestFixture
{
static TestFixture()
Expand Down Expand Up @@ -486,6 +511,7 @@ public override void ConfigureJobHost(IWebJobsBuilder webJobsBuilder)
"HttpTrigger",
"HttpTrigger-CustomRoute-Get",
"HttpTrigger-Disabled",
"HttpTrigger-Identities",
"ManualTrigger"
};
});
Expand Down
2 changes: 1 addition & 1 deletion test/WebJobs.Script.Tests/FunctionMetadataManagerTests.cs
Expand Up @@ -229,7 +229,7 @@ public void ReadFunctionMetadata_Succeeds()
var functionErrors = new Dictionary<string, ICollection<string>>();
var functionDirectories = Directory.EnumerateDirectories(functionsPath);
var metadata = FunctionMetadataManager.ReadFunctionsMetadata(functionDirectories, null, TestHelpers.GetTestWorkerConfigs(), NullLogger.Instance, functionErrors);
Assert.Equal(17, metadata.Count);
Assert.Equal(18, metadata.Count);
}

[Theory]
Expand Down

0 comments on commit 6e34b98

Please sign in to comment.