Skip to content

Commit 0fc45ab

Browse files
committed
Adding Authorization to AdminController (now requires master key header)
1 parent 136f0ef commit 0fc45ab

File tree

24 files changed

+380
-26
lines changed

24 files changed

+380
-26
lines changed

sample/WebHook-Generic/function.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"input": [
44
{
55
"type": "httpTrigger",
6-
"webHookReceiver": "genericJson"
6+
"webHookType": "genericJson"
77
}
88
],
99
"output": [

sample/WebHook-GitHub/function.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"input": [
44
{
55
"type": "httpTrigger",
6-
"webHookReceiver": "github"
6+
"webHookType": "github"
77
}
88
],
99
"output": [
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"key": "hyexydhln844f2mb7hgsup2yf8dowlb0885mbiq1"
3+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"webHookReceiverKey": "1388a6b0d05eca2237f10e4a4641260b0a08f3a5"
2+
"key": "1388a6b0d05eca2237f10e4a4641260b0a08f3a5"
33
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"masterKey": "t8laajal0a1ajkgzoqlfv5gxr4ebhqozebw4qzdy",
3+
"functionKey": "zlnu496ve212kk1p84ncrtdvmtpembduqp25ajjc"
4+
}

src/WebJobs.Script.WebHost/Controllers/AdminController.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Web.Http;
77
using Microsoft.Azure.WebJobs.Script;
88
using Microsoft.Azure.WebJobs.Script.Description;
9+
using WebJobs.Script.WebHost.Filters;
910
using WebJobs.Script.WebHost.Models;
1011

1112
namespace WebJobs.Script.WebHost.Controllers
@@ -14,6 +15,7 @@ namespace WebJobs.Script.WebHost.Controllers
1415
/// Controller responsible for handling all administrative requests, for
1516
/// example enqueueing function invocations, etc.
1617
/// </summary>
18+
[AuthorizationLevel(AuthorizationLevel.Admin)]
1719
public class AdminController : ApiController
1820
{
1921
private readonly WebScriptHostManager _scriptHostManager;
@@ -27,9 +29,6 @@ public AdminController(WebScriptHostManager scriptHostManager)
2729
[Route("admin/functions/{name}")]
2830
public HttpResponseMessage Invoke(string name, [FromBody] FunctionInvocation invocation)
2931
{
30-
// TODO: This entire controller will need to be locked down once the
31-
// admin auth model is in place
32-
3332
if (invocation == null)
3433
{
3534
return new HttpResponseMessage(HttpStatusCode.BadRequest);

src/WebJobs.Script.WebHost/Controllers/FunctionsController.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Web.Http;
88
using System.Web.Http.Controllers;
99
using Microsoft.Azure.WebJobs.Script.Description;
10+
using WebJobs.Script.WebHost.Filters;
1011
using WebJobs.Script.WebHost.WebHooks;
1112

1213
namespace WebJobs.Script.WebHost.Controllers
@@ -36,9 +37,17 @@ public override async Task<HttpResponseMessage> ExecuteAsync(HttpControllerConte
3637
return new HttpResponseMessage(HttpStatusCode.NotFound);
3738
}
3839

39-
HttpResponseMessage response = null;
40+
// Authorize the request
41+
SecretManager secretManager = (SecretManager)controllerContext.Configuration.DependencyResolver.GetService(typeof(SecretManager));
4042
HttpBindingMetadata httpFunctionMetadata = (HttpBindingMetadata)function.Metadata.InputBindings.FirstOrDefault(p => p.Type == BindingType.HttpTrigger);
41-
if (!string.IsNullOrEmpty(httpFunctionMetadata.WebHookReceiver))
43+
bool isWebHook = !string.IsNullOrEmpty(httpFunctionMetadata.WebHookType);
44+
if (!isWebHook && !AuthorizationLevelAttribute.IsAuthorized(request, httpFunctionMetadata.AuthLevel, secretManager, functionName: function.Name))
45+
{
46+
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
47+
}
48+
49+
HttpResponseMessage response = null;
50+
if (isWebHook)
4251
{
4352
// This is a WebHook request so define a delegate for the user function.
4453
// The WebHook Receiver pipeline will first validate the request fully
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Runtime.CompilerServices;
7+
using System.Web.Http.Controllers;
8+
using System.Web.Http.Filters;
9+
using Microsoft.Azure.WebJobs.Script;
10+
11+
namespace WebJobs.Script.WebHost.Filters
12+
{
13+
public class AuthorizationLevelAttribute : AuthorizationFilterAttribute
14+
{
15+
public const string MasterKeyHeaderName = "x-functions-key";
16+
17+
public AuthorizationLevelAttribute(AuthorizationLevel level)
18+
{
19+
Level = level;
20+
}
21+
22+
public AuthorizationLevel Level { get; private set; }
23+
24+
public override void OnAuthorization(HttpActionContext actionContext)
25+
{
26+
SecretManager secretManager = (SecretManager)actionContext.ControllerContext.Configuration.DependencyResolver.GetService(typeof(SecretManager));
27+
28+
if (!IsAuthorized(actionContext.Request, Level, secretManager))
29+
{
30+
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
31+
}
32+
}
33+
34+
public static bool IsAuthorized(HttpRequestMessage request, AuthorizationLevel level, SecretManager secretManager, string functionName = null)
35+
{
36+
if (level == AuthorizationLevel.Anonymous)
37+
{
38+
return true;
39+
}
40+
41+
AuthorizationLevel requestLevel = GetAuthorizationLevel(request, secretManager, functionName);
42+
return requestLevel >= level;
43+
}
44+
45+
private static AuthorizationLevel GetAuthorizationLevel(HttpRequestMessage request, SecretManager secretManager, string functionName = null)
46+
{
47+
// TODO: Add support for validating "EasyAuth" headers
48+
49+
// first see if a key value is specified via headers or query string
50+
IEnumerable<string> values;
51+
string keyValue = null;
52+
if (request.Headers.TryGetValues(MasterKeyHeaderName, out values))
53+
{
54+
keyValue = values.FirstOrDefault();
55+
}
56+
else
57+
{
58+
var queryParameters = request.GetQueryNameValuePairs().ToDictionary(p => p.Key, p => p.Value, StringComparer.OrdinalIgnoreCase);
59+
queryParameters.TryGetValue("key", out keyValue);
60+
}
61+
62+
if (!string.IsNullOrEmpty(keyValue))
63+
{
64+
// see if the key specified is the master key
65+
HostSecrets hostSecrets = secretManager.GetHostSecrets();
66+
if (!string.IsNullOrEmpty(hostSecrets.MasterKey) &&
67+
SecretEqual(keyValue, hostSecrets.MasterKey))
68+
{
69+
return AuthorizationLevel.Admin;
70+
}
71+
72+
// see if the key specified matches the host function key
73+
if (!string.IsNullOrEmpty(hostSecrets.FunctionKey) &&
74+
SecretEqual(keyValue, hostSecrets.FunctionKey))
75+
{
76+
return AuthorizationLevel.Function;
77+
}
78+
79+
// if there is a function specific key specified try to match against that
80+
if (functionName != null)
81+
{
82+
FunctionSecrets functionSecrets = secretManager.GetFunctionSecrets(functionName);
83+
if (functionSecrets != null &&
84+
!string.IsNullOrEmpty(functionSecrets.Key) &&
85+
SecretEqual(keyValue, functionSecrets.Key))
86+
{
87+
return AuthorizationLevel.Function;
88+
}
89+
}
90+
}
91+
92+
return AuthorizationLevel.Anonymous;
93+
}
94+
95+
/// <summary>
96+
/// Provides a time consistent comparison of two secrets in the form of two strings.
97+
/// This prevents security attacks that attempt to determine key values based on response
98+
/// times.
99+
/// </summary>
100+
/// <param name="inputA">The first secret to compare.</param>
101+
/// <param name="inputB">The second secret to compare.</param>
102+
/// <returns>Returns <c>true</c> if the two secrets are equal, <c>false</c> otherwise.</returns>
103+
[MethodImpl(MethodImplOptions.NoOptimization)]
104+
private static bool SecretEqual(string inputA, string inputB)
105+
{
106+
if (ReferenceEquals(inputA, inputB))
107+
{
108+
return true;
109+
}
110+
111+
if (inputA == null || inputB == null || inputA.Length != inputB.Length)
112+
{
113+
return false;
114+
}
115+
116+
bool areSame = true;
117+
for (int i = 0; i < inputA.Length; i++)
118+
{
119+
areSame &= inputA[i] == inputB[i];
120+
}
121+
122+
return areSame;
123+
}
124+
}
125+
}

src/WebJobs.Script.WebHost/FunctionSecrets.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
{
33
public class FunctionSecrets
44
{
5-
public string WebHookReceiverKey { get; set; }
5+
public string Key { get; set; }
66
}
77
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace WebJobs.Script.WebHost
2+
{
3+
public class HostSecrets
4+
{
5+
public string MasterKey { get; set; }
6+
public string FunctionKey { get; set; }
7+
}
8+
}

0 commit comments

Comments
 (0)