Skip to content

Commit 80f5ebc

Browse files
authored
Allow site auth JWT tokens to be passed via x-ms-site-token header. (#9168)
1 parent cacd18b commit 80f5ebc

File tree

15 files changed

+300
-98
lines changed

15 files changed

+300
-98
lines changed

src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ internal HttpRequestMessage BuildSetTriggersRequest()
708708
var requestId = Guid.NewGuid().ToString();
709709
request.Headers.Add(ScriptConstants.AntaresLogIdHeaderName, requestId);
710710
request.Headers.Add("User-Agent", ScriptConstants.FunctionsUserAgent);
711-
request.Headers.Add(ScriptConstants.SiteTokenHeaderName, token);
711+
request.Headers.Add(ScriptConstants.SiteRestrictedTokenHeaderName, token);
712712
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
713713

714714
if (_environment.IsManagedAppEnvironment())

src/WebJobs.Script.WebHost/Metrics/LinuxContainerMetricsPublisher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ private HttpRequestMessage BuildRequest<TContent>(HttpMethod method, string path
289289
// add the required authentication headers
290290
request.Headers.Add(ContainerNameHeader, _containerName);
291291
request.Headers.Add(HostNameHeader, _hostNameProvider.Value);
292-
request.Headers.Add(ScriptConstants.SiteTokenHeaderName, token);
292+
request.Headers.Add(ScriptConstants.SiteRestrictedTokenHeaderName, token);
293293
request.Headers.Add(StampNameHeader, _stampName);
294294

295295
return request;

src/WebJobs.Script.WebHost/Security/Authentication/Arm/ArmAuthenticationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
3535
private AuthenticateResult HandleAuthenticate()
3636
{
3737
string token = null;
38-
if (!Context.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
38+
if (!Context.Request.Headers.TryGetValue(ScriptConstants.SiteRestrictedTokenHeaderName, out StringValues values))
3939
{
4040
return AuthenticateResult.NoResult();
4141
}

src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
using System;
4+
using System.Linq;
55
using System.Security.Claims;
6-
using System.Text;
76
using System.Threading;
87
using System.Threading.Tasks;
98
using Microsoft.AspNetCore.Authentication;
109
using Microsoft.AspNetCore.Authentication.JwtBearer;
11-
using Microsoft.Azure.Web.DataProtection;
1210
using Microsoft.Azure.WebJobs.Extensions.Http;
1311
using Microsoft.Azure.WebJobs.Script;
1412
using Microsoft.Azure.WebJobs.Script.Config;
1513
using Microsoft.Azure.WebJobs.Script.WebHost;
1614
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication;
15+
using Microsoft.Extensions.Primitives;
1716
using Microsoft.IdentityModel.Tokens;
1817
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
1918
using static Microsoft.Azure.WebJobs.Script.ScriptConstants;
@@ -31,8 +30,16 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
3130
{
3231
OnMessageReceived = c =>
3332
{
33+
// By default, tokens are passed via the standard Authorization Bearer header. However we also support
34+
// passing tokens via the x-ms-site-token header.
35+
if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
36+
{
37+
// the token we set here will be the one used - Authorization header won't be checked.
38+
c.Token = values.FirstOrDefault();
39+
}
40+
3441
// Temporary: Tactical fix to address specialization issues. This should likely be moved to a token validator
35-
// TODO: DI (FACAVAL) This will be fixed once the permanent fix is in plance
42+
// TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
3643
if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
3744
{
3845
o.TokenValidationParameters = CreateTokenValidationParameters();
@@ -55,7 +62,7 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
5562

5663
o.TokenValidationParameters = CreateTokenValidationParameters();
5764

58-
// TODO: DI (FACAVAL) Remove this once th work above is completed.
65+
// TODO: DI (FACAVAL) Remove this once the work above is completed.
5966
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
6067
{
6168
// We're not in standby mode, so flag as specialized
@@ -65,17 +72,23 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
6572

6673
private static TokenValidationParameters CreateTokenValidationParameters()
6774
{
68-
string defaultKey = Util.GetDefaultKeyValue();
69-
7075
var result = new TokenValidationParameters();
71-
if (defaultKey != null)
76+
if (SecretsUtility.TryGetEncryptionKey(out byte[] key))
7277
{
7378
// TODO: Once ScriptSettingsManager is gone, Audience and Issuer shouold be pulled from configuration.
74-
result.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(defaultKey));
79+
result.IssuerSigningKey = new SymmetricSecurityKey(key);
7580
result.ValidateAudience = true;
7681
result.ValidateIssuer = true;
77-
result.ValidAudience = string.Format(AdminJwtValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName));
78-
result.ValidIssuer = string.Format(AdminJwtValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName));
82+
result.ValidAudiences = new string[]
83+
{
84+
string.Format(AdminJwtValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
85+
AdminJwtAppServiceIssuer
86+
};
87+
result.ValidIssuers = new string[]
88+
{
89+
string.Format(AdminJwtValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
90+
AdminJwtAppServiceIssuer
91+
};
7992
}
8093

8194
return result;

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

Lines changed: 76 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,78 @@ public static string GetNonDecryptableName(string secretsPath)
1718
}
1819
return secretsPath + $".{ScriptConstants.Snapshot}.{timeStamp}.json";
1920
}
21+
22+
public static bool TryGetEncryptionKey(out byte[] key, IEnvironment environment = null)
23+
{
24+
environment = environment ?? SystemEnvironment.Instance;
25+
26+
if (environment.IsKubernetesManagedHosting())
27+
{
28+
var podEncryptionKey = environment.GetEnvironmentVariable(EnvironmentSettingNames.PodEncryptionKey);
29+
if (!string.IsNullOrEmpty(podEncryptionKey))
30+
{
31+
key = Convert.FromBase64String(podEncryptionKey);
32+
return true;
33+
}
34+
}
35+
36+
// Use WebSiteAuthEncryptionKey if available else fall back to ContainerEncryptionKey.
37+
// Until the container is specialized to a specific site WebSiteAuthEncryptionKey will not be available.
38+
if (TryGetEncryptionKey(environment, EnvironmentSettingNames.WebSiteAuthEncryptionKey, out key) ||
39+
TryGetEncryptionKey(environment, EnvironmentSettingNames.ContainerEncryptionKey, out key))
40+
{
41+
return true;
42+
}
43+
44+
// Fall back to using DataProtection APIs to get the key
45+
string defaultKey = Util.GetDefaultKeyValue();
46+
if (!string.IsNullOrEmpty(defaultKey))
47+
{
48+
key = defaultKey.ToKeyBytes();
49+
return true;
50+
}
51+
52+
return false;
53+
}
54+
55+
public static byte[] GetEncryptionKey(IEnvironment environment = null)
56+
{
57+
if (TryGetEncryptionKey(out byte[] key, environment))
58+
{
59+
return key;
60+
}
61+
else
62+
{
63+
throw new InvalidOperationException($"No encryption key defined in the environment.");
64+
}
65+
}
66+
67+
public static byte[] ToKeyBytes(this string hexOrBase64)
68+
{
69+
// only support 32 bytes (256 bits) key length
70+
if (hexOrBase64.Length == 64)
71+
{
72+
return Enumerable.Range(0, hexOrBase64.Length)
73+
.Where(x => x % 2 == 0)
74+
.Select(x => Convert.ToByte(hexOrBase64.Substring(x, 2), 16))
75+
.ToArray();
76+
}
77+
78+
return Convert.FromBase64String(hexOrBase64);
79+
}
80+
81+
private static bool TryGetEncryptionKey(IEnvironment environment, string keyName, out byte[] encryptionKey)
82+
{
83+
encryptionKey = null;
84+
var hexOrBase64 = environment.GetEnvironmentVariable(keyName);
85+
if (string.IsNullOrEmpty(hexOrBase64))
86+
{
87+
return false;
88+
}
89+
90+
encryptionKey = hexOrBase64.ToKeyBytes();
91+
92+
return true;
93+
}
2094
}
2195
}

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

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ public static class SimpleWebTokenHelper
2424

2525
internal static string Encrypt(string value, byte[] key = null, IEnvironment environment = null)
2626
{
27-
if (key == null)
28-
{
29-
TryGetEncryptionKey(environment, EnvironmentSettingNames.WebSiteAuthEncryptionKey, out key);
30-
}
27+
key = key ?? SecretsUtility.GetEncryptionKey(environment);
3128

3229
using (var aes = Aes.Create())
3330
{
@@ -90,19 +87,7 @@ public static string Decrypt(byte[] encryptionKey, string value)
9087

9188
public static string Decrypt(string value, IEnvironment environment = null)
9289
{
93-
if ((environment != null) && environment.IsKubernetesManagedHosting())
94-
{
95-
var encryptionKey = GetPodEncryptionKey(environment);
96-
return Decrypt(encryptionKey, value);
97-
}
98-
// Use WebSiteAuthEncryptionKey if available else fallback to ContainerEncryptionKey.
99-
// Until the container is specialized to a specific site WebSiteAuthEncryptionKey will not be available.
100-
byte[] key;
101-
if (!TryGetEncryptionKey(environment, EnvironmentSettingNames.WebSiteAuthEncryptionKey, out key, false))
102-
{
103-
TryGetEncryptionKey(environment, EnvironmentSettingNames.ContainerEncryptionKey, out key);
104-
}
105-
90+
byte[] key = SecretsUtility.GetEncryptionKey(environment);
10691
return Decrypt(key, value);
10792
}
10893

@@ -140,50 +125,5 @@ private static string GetSHA256Base64String(byte[] key)
140125
return Convert.ToBase64String(sha256.ComputeHash(key));
141126
}
142127
}
143-
144-
private static bool TryGetEncryptionKey(IEnvironment environment, string keyName, out byte[] encryptionKey, bool throwIfFailed = true)
145-
{
146-
environment = environment ?? SystemEnvironment.Instance;
147-
148-
encryptionKey = null;
149-
var hexOrBase64 = environment.GetEnvironmentVariable(keyName);
150-
if (string.IsNullOrEmpty(hexOrBase64))
151-
{
152-
if (throwIfFailed)
153-
{
154-
throw new InvalidOperationException($"No {keyName} defined in the environment");
155-
}
156-
157-
return false;
158-
}
159-
160-
encryptionKey = hexOrBase64.ToKeyBytes();
161-
162-
return true;
163-
}
164-
165-
private static byte[] GetPodEncryptionKey(IEnvironment environment)
166-
{
167-
var podEncryptionKey = environment.GetEnvironmentVariable(EnvironmentSettingNames.PodEncryptionKey);
168-
if (string.IsNullOrEmpty(podEncryptionKey))
169-
{
170-
throw new Exception("Pod encryption key is empty.");
171-
}
172-
return Convert.FromBase64String(podEncryptionKey);
173-
}
174-
175-
public static byte[] ToKeyBytes(this string hexOrBase64)
176-
{
177-
// only support 32 bytes (256 bits) key length
178-
if (hexOrBase64.Length == 64)
179-
{
180-
return Enumerable.Range(0, hexOrBase64.Length)
181-
.Where(x => x % 2 == 0)
182-
.Select(x => Convert.ToByte(hexOrBase64.Substring(x, 2), 16))
183-
.ToArray();
184-
}
185-
186-
return Convert.FromBase64String(hexOrBase64);
187-
}
188128
}
189129
}

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ public static class ScriptConstants
100100
public const string AntaresLogIdHeaderName = "X-ARR-LOG-ID";
101101
public const string AntaresScaleOutHeaderName = "X-FUNCTION-SCALEOUT";
102102
public const string AntaresColdStartHeaderName = "X-MS-COLDSTART";
103-
public const string SiteTokenHeaderName = "x-ms-site-restricted-token";
103+
public const string SiteRestrictedTokenHeaderName = "x-ms-site-restricted-token";
104+
public const string SiteTokenHeaderName = "x-ms-site-token";
104105
public const string EasyAuthIdentityHeader = "x-ms-client-principal";
105106
public const string AntaresPlatformInternal = "x-ms-platform-internal";
106107
public const string AzureVersionHeader = "x-ms-version";
@@ -129,6 +130,7 @@ public static class ScriptConstants
129130

130131
public const string AdminJwtValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions";
131132
public const string AdminJwtValidIssuerFormat = "https://{0}.scm.azurewebsites.net";
133+
public const string AdminJwtAppServiceIssuer = "https://appservice.core.azurewebsites.net";
132134

133135
public const string AzureFunctionsSystemDirectoryName = ".azurefunctions";
134136
public const string HttpMethodConstraintName = "httpMethod";

test/WebJobs.Script.Tests.Integration/Management/KubernetesPodControllerTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Net;
67
using System.Net.Http;
@@ -138,7 +139,7 @@ public async Task Assignment_Fails_Without_Encryption_Key()
138139
environment.SetEnvironmentVariable(EnvironmentSettingNames.KubernetesServiceHost, "http://localhost:80");
139140
environment.SetEnvironmentVariable(EnvironmentSettingNames.PodNamespace, "k8se-apps");
140141

141-
var ex = await Assert.ThrowsAsync<System.Exception>(async () =>
142+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
142143
{
143144
await (podController.Assign(encryptedHostAssignmentContext));
144145
});

test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,15 +409,14 @@ public async Task<HostStatus> GetHostStatusAsync()
409409
return await response.Content.ReadAsAsync<HostStatus>();
410410
}
411411

412-
public string GenerateAdminJwtToken()
412+
public string GenerateAdminJwtToken(string audience = null, string issuer = null)
413413
{
414414
var tokenHandler = new JwtSecurityTokenHandler();
415-
string defaultKey = Util.GetDefaultKeyValue();
416-
var key = Encoding.ASCII.GetBytes(defaultKey);
415+
byte[] key = SecretsUtility.GetEncryptionKey();
417416
var tokenDescriptor = new SecurityTokenDescriptor
418417
{
419-
Audience = string.Format(ScriptConstants.AdminJwtValidAudienceFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
420-
Issuer = string.Format(ScriptConstants.AdminJwtValidIssuerFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
418+
Audience = audience ?? string.Format(ScriptConstants.AdminJwtValidAudienceFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
419+
Issuer = issuer ?? string.Format(ScriptConstants.AdminJwtValidIssuerFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
421420
Expires = DateTime.UtcNow.AddHours(1),
422421
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
423422
};

0 commit comments

Comments
 (0)