Skip to content

Commit e458c41

Browse files
authored
Allow site auth JWT tokens to be passed via x-ms-site-token header (#9176)
1 parent 39c9bbd commit e458c41

File tree

7 files changed

+187
-90
lines changed

7 files changed

+187
-90
lines changed

src/WebJobs.Script.WebHost/Filters/JwtAuthenticationAttribute.cs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
57
using System.Net.Http.Headers;
68
using System.Text;
79
using System.Threading;
810
using System.Threading.Tasks;
911
using System.Web.Http.Filters;
1012
using Microsoft.Azure.Web.DataProtection;
1113
using Microsoft.Azure.WebJobs.Extensions.Http;
14+
using Microsoft.Azure.WebJobs.Script.Config;
1215
using Microsoft.IdentityModel.Tokens;
13-
using static Microsoft.Azure.WebJobs.Script.Config.ScriptSettingsManager;
1416
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
1517
using static Microsoft.Azure.WebJobs.Script.ScriptConstants;
1618

@@ -23,23 +25,50 @@ public sealed class JwtAuthenticationAttribute : Attribute, IAuthenticationFilte
2325

2426
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
2527
{
26-
AuthenticationHeaderValue authorization = context.Request.Headers.Authorization;
28+
// By default, tokens are passed via the standard Authorization Bearer header. However we also support
29+
// passing tokens via the x-ms-site-token header.
30+
string token = null;
31+
if (context.Request.Headers.TryGetValues(ScriptConstants.SiteTokenHeaderName, out IEnumerable<string> values))
32+
{
33+
token = values.FirstOrDefault();
34+
}
35+
else
36+
{
37+
// check the standard Authorization header
38+
AuthenticationHeaderValue authorization = context.Request.Headers.Authorization;
39+
if (authorization != null && string.Equals(authorization.Scheme, "Bearer", StringComparison.OrdinalIgnoreCase))
40+
{
41+
token = authorization.Parameter;
42+
}
43+
}
2744

28-
if (authorization != null && string.Equals(authorization.Scheme, "Bearer", StringComparison.OrdinalIgnoreCase))
45+
if (!string.IsNullOrEmpty(token))
2946
{
30-
string defaultKey = Util.GetDefaultKeyValue();
31-
if (defaultKey != null)
47+
if (SecretsUtility.TryGetEncryptionKey(out string key))
3248
{
3349
var validationParameters = new TokenValidationParameters()
3450
{
35-
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(defaultKey)),
51+
IssuerSigningKeys = new SecurityKey[]
52+
{
53+
new SymmetricSecurityKey(key.ToKeyBytes()),
54+
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
55+
},
3656
ValidateAudience = true,
3757
ValidateIssuer = true,
38-
ValidAudience = string.Format(AdminJwtValidAudienceFormat, Instance.GetSetting(AzureWebsiteName)),
39-
ValidIssuer = string.Format(AdminJwtValidIssuerFormat, Instance.GetSetting(AzureWebsiteName))
58+
ValidAudiences = new string[]
59+
{
60+
string.Format(AdminJwtSiteFunctionsValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
61+
string.Format(AdminJwtSiteValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
62+
},
63+
ValidIssuers = new string[]
64+
{
65+
AdminJwtAppServiceIssuer,
66+
string.Format(AdminJwtScmValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
67+
string.Format(AdminJwtSiteValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
68+
}
4069
};
4170

42-
if (JwtGenerator.IsTokenValid(authorization.Parameter, validationParameters))
71+
if (JwtGenerator.IsTokenValid(token, validationParameters))
4372
{
4473
context.Request.SetAuthorizationLevel(AuthorizationLevel.Admin);
4574
}

src/WebJobs.Script.WebHost/Security/SecretsUtility.cs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.IO;
5+
using System.Linq;
6+
using Microsoft.Azure.Web.DataProtection;
67

78
namespace Microsoft.Azure.WebJobs.Script.WebHost
89
{
9-
internal class SecretsUtility
10+
internal static class SecretsUtility
1011
{
1112
public static string GetNonDecryptableName(string secretsPath)
1213
{
@@ -17,5 +18,60 @@ public static string GetNonDecryptableName(string secretsPath)
1718
}
1819
return secretsPath + $".{ScriptConstants.Snapshot}.{timeStamp}.json";
1920
}
21+
22+
public static bool TryGetEncryptionKey(out string key)
23+
{
24+
if (TryGetEncryptionKey(EnvironmentSettingNames.WebsiteAuthEncryptionKey, out key))
25+
{
26+
return true;
27+
}
28+
29+
// Fall back to using DataProtection APIs to get the key
30+
key = Util.GetDefaultKeyValue();
31+
if (!string.IsNullOrEmpty(key))
32+
{
33+
return true;
34+
}
35+
36+
return false;
37+
}
38+
39+
public static string GetEncryptionKeyValue()
40+
{
41+
if (TryGetEncryptionKey(out string key))
42+
{
43+
return key;
44+
}
45+
else
46+
{
47+
throw new InvalidOperationException($"No encryption key defined in the environment.");
48+
}
49+
}
50+
51+
public static byte[] GetEncryptionKey()
52+
{
53+
string key = GetEncryptionKeyValue();
54+
return key.ToKeyBytes();
55+
}
56+
57+
public static bool TryGetEncryptionKey(string keyName, out string encryptionKey)
58+
{
59+
encryptionKey = Environment.GetEnvironmentVariable(keyName);
60+
return !string.IsNullOrEmpty(encryptionKey);
61+
}
62+
63+
public static byte[] ToKeyBytes(this string hexOrBase64)
64+
{
65+
// only support 32 bytes (256 bits) key length
66+
if (hexOrBase64.Length == 64)
67+
{
68+
return Enumerable.Range(0, hexOrBase64.Length)
69+
.Where(x => x % 2 == 0)
70+
.Select(x => Convert.ToByte(hexOrBase64.Substring(x, 2), 16))
71+
.ToArray();
72+
}
73+
74+
return Convert.FromBase64String(hexOrBase64);
75+
}
2076
}
2177
}

src/WebJobs.Script.WebHost/Security/SimpleWebTokenHelper.cs

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ internal static class SimpleWebTokenHelper
2323

2424
internal static string Encrypt(string value, byte[] key = null)
2525
{
26-
if (key == null)
27-
{
28-
TryGetEncryptionKey(EnvironmentSettingNames.WebsiteAuthEncryptionKey, out key);
29-
}
26+
key = key ?? SecretsUtility.GetEncryptionKey();
3027

3128
using (var aes = new AesManaged { Key = key })
3229
{
@@ -97,8 +94,7 @@ public static bool TryValidateToken(string token, DateTime dateTime)
9794

9895
public static bool ValidateToken(string token, DateTime dateTime)
9996
{
100-
// Use WebSiteAuthEncryptionKey if available.
101-
TryGetEncryptionKey(EnvironmentSettingNames.WebsiteAuthEncryptionKey, out byte[] key);
97+
byte[] key = SecretsUtility.GetEncryptionKey();
10298

10399
var data = Decrypt(key, token);
104100

@@ -120,38 +116,5 @@ private static string GetSHA256Base64String(byte[] key)
120116
return Convert.ToBase64String(sha256.ComputeHash(key));
121117
}
122118
}
123-
124-
public static bool TryGetEncryptionKey(string keyName, out byte[] encryptionKey, bool throwIfFailed = true)
125-
{
126-
encryptionKey = null;
127-
var hexOrBase64 = Environment.GetEnvironmentVariable(keyName);
128-
if (string.IsNullOrEmpty(hexOrBase64))
129-
{
130-
if (throwIfFailed)
131-
{
132-
throw new InvalidOperationException($"No {keyName} defined in the environment");
133-
}
134-
135-
return false;
136-
}
137-
138-
encryptionKey = hexOrBase64.ToKeyBytes();
139-
140-
return true;
141-
}
142-
143-
public static byte[] ToKeyBytes(this string hexOrBase64)
144-
{
145-
// only support 32 bytes (256 bits) key length
146-
if (hexOrBase64.Length == 64)
147-
{
148-
return Enumerable.Range(0, hexOrBase64.Length)
149-
.Where(x => x % 2 == 0)
150-
.Select(x => Convert.ToByte(hexOrBase64.Substring(x, 2), 16))
151-
.ToArray();
152-
}
153-
154-
return Convert.FromBase64String(hexOrBase64);
155-
}
156119
}
157120
}

src/WebJobs.Script/GlobalSuppressions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,5 @@
248248
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dir", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Scale.ApplicationPerformanceCounters.#RemoteDirMonitors")]
249249
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sas", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames.#AzureWebJobsSecretStorageSas")]
250250
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ARM", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptConstants.#AntaresARMRequestTrackingIdHeader")]
251-
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ARM", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptConstants.#AntaresARMExtensionsRouteHeader")]
251+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ARM", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptConstants.#AntaresARMExtensionsRouteHeader")]
252+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Scm", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptConstants.#AdminJwtScmValidIssuerFormat")]

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public static class ScriptConstants
6565
public const string ColdStartEventName = "ColdStart";
6666
public const string ShutdownRecoveryEventName = "ShutdownRecovery";
6767

68+
public const string SiteTokenHeaderName = "x-ms-site-token";
6869
public const string AntaresARMRequestTrackingIdHeader = "x-ms-arm-request-tracking-id";
6970
public const string AntaresARMExtensionsRouteHeader = "X-MS-VIA-EXTENSIONS-ROUTE";
7071
public const string AntaresClientAuthorizationSourceHeader = "X-MS-CLIENT-AUTHORIZATION-SOURCE";
@@ -82,8 +83,11 @@ public static class ScriptConstants
8283
public const string FeatureFlagDisableShadowCopy = "DisableShadowCopy";
8384
public const string FeatureFlagsEnableDynamicExtensionLoading = "EnableDynamicExtensionLoading";
8485

85-
public const string AdminJwtValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions";
86-
public const string AdminJwtValidIssuerFormat = "https://{0}.scm.azurewebsites.net";
86+
public const string AdminJwtSiteFunctionsValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions";
87+
public const string AdminJwtSiteValidAudienceFormat = "https://{0}.azurewebsites.net";
88+
public const string AdminJwtScmValidIssuerFormat = "https://{0}.scm.azurewebsites.net";
89+
public const string AdminJwtSiteValidIssuerFormat = "https://{0}.azurewebsites.net";
90+
public const string AdminJwtAppServiceIssuer = "https://appservice.core.azurewebsites.net";
8791

8892
public const string SwaggerFileName = "swagger.json";
8993
public const string AzureFunctionsSystemDirectoryName = ".azurefunctions";

test/WebJobs.Script.Tests/Filters/AuthorizationLevelAttributeTests.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Net.Http;
99
using System.Reflection;
1010
using System.Security.Cryptography;
11+
using System.ServiceModel.Channels;
1112
using System.Text;
1213
using System.Threading;
1314
using System.Threading.Tasks;
@@ -498,10 +499,7 @@ public static string GenerateKeyHexString(byte[] key = null)
498499

499500
private static string EncryptSimpleWebToken(string value, byte[] key = null)
500501
{
501-
if (key == null)
502-
{
503-
SimpleWebTokenHelper.TryGetEncryptionKey(EnvironmentSettingNames.WebsiteAuthEncryptionKey, out key);
504-
}
502+
key = key ?? SecretsUtility.GetEncryptionKey();
505503

506504
using (var aes = new AesManaged { Key = key })
507505
{

0 commit comments

Comments
 (0)