Skip to content

Commit a084951

Browse files
authored
Allow site auth JWT tokens to be passed via x-ms-site-token header (#9175)
1 parent 4df79eb commit a084951

File tree

16 files changed

+398
-106
lines changed

16 files changed

+398
-106
lines changed

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

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

713713
if (_environment.IsKubernetesManagedHosting())

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: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
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;
66
using System.Text;
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Authentication;
1010
using Microsoft.AspNetCore.Authentication.JwtBearer;
11-
using Microsoft.Azure.Web.DataProtection;
1211
using Microsoft.Azure.WebJobs.Extensions.Http;
1312
using Microsoft.Azure.WebJobs.Script;
1413
using Microsoft.Azure.WebJobs.Script.Config;
1514
using Microsoft.Azure.WebJobs.Script.WebHost;
1615
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication;
16+
using Microsoft.Extensions.Primitives;
1717
using Microsoft.IdentityModel.Tokens;
1818
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
1919
using static Microsoft.Azure.WebJobs.Script.ScriptConstants;
@@ -31,8 +31,16 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
3131
{
3232
OnMessageReceived = c =>
3333
{
34+
// By default, tokens are passed via the standard Authorization Bearer header. However we also support
35+
// passing tokens via the x-ms-site-token header.
36+
if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
37+
{
38+
// the token we set here will be the one used - Authorization header won't be checked.
39+
c.Token = values.FirstOrDefault();
40+
}
41+
3442
// 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
43+
// TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
3644
if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
3745
{
3846
o.TokenValidationParameters = CreateTokenValidationParameters();
@@ -55,7 +63,7 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
5563

5664
o.TokenValidationParameters = CreateTokenValidationParameters();
5765

58-
// TODO: DI (FACAVAL) Remove this once th work above is completed.
66+
// TODO: DI (FACAVAL) Remove this once the work above is completed.
5967
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
6068
{
6169
// We're not in standby mode, so flag as specialized
@@ -65,17 +73,28 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
6573

6674
private static TokenValidationParameters CreateTokenValidationParameters()
6775
{
68-
string defaultKey = Util.GetDefaultKeyValue();
69-
7076
var result = new TokenValidationParameters();
71-
if (defaultKey != null)
77+
if (SecretsUtility.TryGetEncryptionKey(out string key))
7278
{
73-
// TODO: Once ScriptSettingsManager is gone, Audience and Issuer shouold be pulled from configuration.
74-
result.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(defaultKey));
79+
// TODO: Once ScriptSettingsManager is gone, Audience and Issuer should be pulled from configuration.
80+
result.IssuerSigningKeys = new SecurityKey[]
81+
{
82+
new SymmetricSecurityKey(key.ToKeyBytes()),
83+
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
84+
};
7585
result.ValidateAudience = true;
7686
result.ValidateIssuer = true;
77-
result.ValidAudience = string.Format(AdminJwtValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName));
78-
result.ValidIssuer = string.Format(AdminJwtValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName));
87+
result.ValidAudiences = new string[]
88+
{
89+
string.Format(AdminJwtSiteFunctionsValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
90+
string.Format(AdminJwtSiteValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
91+
};
92+
result.ValidIssuers = new string[]
93+
{
94+
AdminJwtAppServiceIssuer,
95+
string.Format(AdminJwtScmValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
96+
string.Format(AdminJwtSiteValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
97+
};
7998
}
8099

81100
return result;

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

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

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 = new AesManaged { Key = key })
3330
{
@@ -86,19 +83,7 @@ public static string Decrypt(byte[] encryptionKey, string value)
8683

8784
public static string Decrypt(string value, IEnvironment environment = null)
8885
{
89-
if ((environment != null) && environment.IsKubernetesManagedHosting())
90-
{
91-
var encryptionKey = GetPodEncryptionKey(environment);
92-
return Decrypt(encryptionKey, value);
93-
}
94-
// Use WebSiteAuthEncryptionKey if available else fallback to ContainerEncryptionKey.
95-
// Until the container is specialized to a specific site WebSiteAuthEncryptionKey will not be available.
96-
byte[] key;
97-
if (!TryGetEncryptionKey(environment, EnvironmentSettingNames.WebSiteAuthEncryptionKey, out key, false))
98-
{
99-
TryGetEncryptionKey(environment, EnvironmentSettingNames.ContainerEncryptionKey, out key);
100-
}
101-
86+
byte[] key = SecretsUtility.GetEncryptionKey(environment);
10287
return Decrypt(key, value);
10388
}
10489

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

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ public static class ScriptConstants
9595
public const string AntaresLogIdHeaderName = "X-ARR-LOG-ID";
9696
public const string AntaresScaleOutHeaderName = "X-FUNCTION-SCALEOUT";
9797
public const string AntaresColdStartHeaderName = "X-MS-COLDSTART";
98-
public const string SiteTokenHeaderName = "x-ms-site-restricted-token";
98+
public const string SiteTokenHeaderName = "x-ms-site-token";
99+
public const string SiteRestrictedTokenHeaderName = "x-ms-site-restricted-token";
99100
public const string EasyAuthIdentityHeader = "x-ms-client-principal";
100101
public const string AntaresPlatformInternal = "x-ms-platform-internal";
101102
public const string AzureVersionHeader = "x-ms-version";
@@ -123,8 +124,11 @@ public static class ScriptConstants
123124
public const string FeatureFlagEnableMultiLanguageWorker = "EnableMultiLanguageWorker";
124125
public const string FeatureFlagEnableLinuxEPExecutionCount = "EnableLinuxFEC";
125126

126-
public const string AdminJwtValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions";
127-
public const string AdminJwtValidIssuerFormat = "https://{0}.scm.azurewebsites.net";
127+
public const string AdminJwtSiteFunctionsValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions";
128+
public const string AdminJwtSiteValidAudienceFormat = "https://{0}.azurewebsites.net";
129+
public const string AdminJwtScmValidIssuerFormat = "https://{0}.scm.azurewebsites.net";
130+
public const string AdminJwtSiteValidIssuerFormat = "https://{0}.azurewebsites.net";
131+
public const string AdminJwtAppServiceIssuer = "https://appservice.core.azurewebsites.net";
128132

129133
public const string AzureFunctionsSystemDirectoryName = ".azurefunctions";
130134
public const string HttpMethodConstraintName = "httpMethod";

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

Lines changed: 3 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,10 +139,11 @@ 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
});
146+
Assert.Equal("No encryption key defined in the environment.", ex.Message);
145147
Assert.Null(startupContextProvider.Context);
146148
}
147149

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
using Microsoft.AspNetCore.Builder;
1616
using Microsoft.AspNetCore.Hosting;
1717
using Microsoft.AspNetCore.TestHost;
18-
using Microsoft.Azure.Web.DataProtection;
1918
using Microsoft.Azure.WebJobs.Host.Executors;
2019
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
2120
using Microsoft.Azure.WebJobs.Script.Models;
@@ -404,15 +403,14 @@ public async Task<HostStatus> GetHostStatusAsync()
404403
return await response.Content.ReadAsAsync<HostStatus>();
405404
}
406405

407-
public string GenerateAdminJwtToken()
406+
public string GenerateAdminJwtToken(string audience = null, string issuer = null, byte[] key = null)
408407
{
409408
var tokenHandler = new JwtSecurityTokenHandler();
410-
string defaultKey = Util.GetDefaultKeyValue();
411-
var key = Encoding.ASCII.GetBytes(defaultKey);
409+
key = key ?? SecretsUtility.GetEncryptionKey();
412410
var tokenDescriptor = new SecurityTokenDescriptor
413411
{
414-
Audience = string.Format(ScriptConstants.AdminJwtValidAudienceFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
415-
Issuer = string.Format(ScriptConstants.AdminJwtValidIssuerFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
412+
Audience = audience ?? string.Format(ScriptConstants.AdminJwtSiteFunctionsValidAudienceFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
413+
Issuer = issuer ?? string.Format(ScriptConstants.AdminJwtScmValidIssuerFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
416414
Expires = DateTime.UtcNow.AddHours(1),
417415
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
418416
};

0 commit comments

Comments
 (0)