Skip to content

Commit a379410

Browse files
authored
Add support for additional key encodings and audience/issuers (#9186)
1 parent 942b79c commit a379410

File tree

7 files changed

+86
-37
lines changed

7 files changed

+86
-37
lines changed

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Linq;
55
using System.Security.Claims;
6+
using System.Text;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.AspNetCore.Authentication;
@@ -73,21 +74,26 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
7374
private static TokenValidationParameters CreateTokenValidationParameters()
7475
{
7576
var result = new TokenValidationParameters();
76-
if (SecretsUtility.TryGetEncryptionKey(out byte[] key))
77+
if (SecretsUtility.TryGetEncryptionKey(out string key))
7778
{
78-
// TODO: Once ScriptSettingsManager is gone, Audience and Issuer shouold be pulled from configuration.
79-
result.IssuerSigningKey = new SymmetricSecurityKey(key);
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+
};
8085
result.ValidateAudience = true;
8186
result.ValidateIssuer = true;
8287
result.ValidAudiences = new string[]
8388
{
84-
string.Format(AdminJwtValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
85-
AdminJwtAppServiceIssuer
89+
string.Format(AdminJwtSiteFunctionsValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
90+
string.Format(AdminJwtSiteValidAudienceFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
8691
};
8792
result.ValidIssuers = new string[]
8893
{
89-
string.Format(AdminJwtValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
90-
AdminJwtAppServiceIssuer
94+
AdminJwtAppServiceIssuer,
95+
string.Format(AdminJwtScmValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
96+
string.Format(AdminJwtSiteValidIssuerFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
9197
};
9298
}
9399

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

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,15 @@ public static string GetNonDecryptableName(string secretsPath)
1919
return secretsPath + $".{ScriptConstants.Snapshot}.{timeStamp}.json";
2020
}
2121

22-
public static bool TryGetEncryptionKey(out byte[] key, IEnvironment environment = null)
22+
public static bool TryGetEncryptionKey(out string key, IEnvironment environment = null)
2323
{
2424
environment = environment ?? SystemEnvironment.Instance;
2525

2626
if (environment.IsKubernetesManagedHosting())
2727
{
28-
var podEncryptionKey = environment.GetEnvironmentVariable(EnvironmentSettingNames.PodEncryptionKey);
29-
if (!string.IsNullOrEmpty(podEncryptionKey))
28+
key = environment.GetEnvironmentVariable(EnvironmentSettingNames.PodEncryptionKey);
29+
if (!string.IsNullOrEmpty(key))
3030
{
31-
key = Convert.FromBase64String(podEncryptionKey);
3231
return true;
3332
}
3433
}
@@ -42,19 +41,18 @@ public static bool TryGetEncryptionKey(out byte[] key, IEnvironment environment
4241
}
4342

4443
// Fall back to using DataProtection APIs to get the key
45-
string defaultKey = Util.GetDefaultKeyValue();
46-
if (!string.IsNullOrEmpty(defaultKey))
44+
key = Util.GetDefaultKeyValue();
45+
if (!string.IsNullOrEmpty(key))
4746
{
48-
key = defaultKey.ToKeyBytes();
4947
return true;
5048
}
5149

5250
return false;
5351
}
5452

55-
public static byte[] GetEncryptionKey(IEnvironment environment = null)
53+
public static string GetEncryptionKeyValue(IEnvironment environment = null)
5654
{
57-
if (TryGetEncryptionKey(out byte[] key, environment))
55+
if (TryGetEncryptionKey(out string key, environment))
5856
{
5957
return key;
6058
}
@@ -64,6 +62,12 @@ public static byte[] GetEncryptionKey(IEnvironment environment = null)
6462
}
6563
}
6664

65+
public static byte[] GetEncryptionKey(IEnvironment environment = null)
66+
{
67+
string key = GetEncryptionKeyValue(environment);
68+
return key.ToKeyBytes();
69+
}
70+
6771
public static byte[] ToKeyBytes(this string hexOrBase64)
6872
{
6973
// only support 32 bytes (256 bits) key length
@@ -78,18 +82,10 @@ public static byte[] ToKeyBytes(this string hexOrBase64)
7882
return Convert.FromBase64String(hexOrBase64);
7983
}
8084

81-
private static bool TryGetEncryptionKey(IEnvironment environment, string keyName, out byte[] encryptionKey)
85+
private static bool TryGetEncryptionKey(IEnvironment environment, string keyName, out string encryptionKey)
8286
{
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;
87+
encryptionKey = environment.GetEnvironmentVariable(keyName);
88+
return !string.IsNullOrEmpty(encryptionKey);
9389
}
9490
}
9591
}

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,10 @@ public static class ScriptConstants
128128
public const string HostingConfigDisableLinuxAppServiceDetailedExecutionEvents = "DisableLinuxExecutionDetails";
129129
public const string HostingConfigDisableLinuxAppServiceExecutionEventLogBackoff = "DisableLinuxLogBackoff";
130130

131-
public const string AdminJwtValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions";
132-
public const string AdminJwtValidIssuerFormat = "https://{0}.scm.azurewebsites.net";
131+
public const string AdminJwtSiteFunctionsValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions";
132+
public const string AdminJwtSiteValidAudienceFormat = "https://{0}.azurewebsites.net";
133+
public const string AdminJwtScmValidIssuerFormat = "https://{0}.scm.azurewebsites.net";
134+
public const string AdminJwtSiteValidIssuerFormat = "https://{0}.azurewebsites.net";
133135
public const string AdminJwtAppServiceIssuer = "https://appservice.core.azurewebsites.net";
134136

135137
public const string AzureFunctionsSystemDirectoryName = ".azurefunctions";

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

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

412-
public string GenerateAdminJwtToken(string audience = null, string issuer = null)
412+
public string GenerateAdminJwtToken(string audience = null, string issuer = null, byte[] key = null)
413413
{
414414
var tokenHandler = new JwtSecurityTokenHandler();
415-
byte[] key = SecretsUtility.GetEncryptionKey();
415+
key = key ?? SecretsUtility.GetEncryptionKey();
416416
var tokenDescriptor = new SecurityTokenDescriptor
417417
{
418-
Audience = audience ?? string.Format(ScriptConstants.AdminJwtValidAudienceFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
419-
Issuer = issuer ?? string.Format(ScriptConstants.AdminJwtValidIssuerFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
418+
Audience = audience ?? string.Format(ScriptConstants.AdminJwtSiteFunctionsValidAudienceFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
419+
Issuer = issuer ?? string.Format(ScriptConstants.AdminJwtScmValidIssuerFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
420420
Expires = DateTime.UtcNow.AddHours(1),
421421
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
422422
};

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/JwtTokenAuthTests.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net;
77
using System.Net.Http;
88
using System.Net.Http.Headers;
9+
using System.Text;
910
using System.Threading.Tasks;
1011
using Microsoft.Azure.WebJobs.Script.WebHost;
1112
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
@@ -30,10 +31,18 @@ public JwtTokenAuthTests(TestFixture fixture)
3031

3132
[Theory]
3233
[InlineData(nameof(HttpRequestHeader.Authorization))]
33-
[InlineData(nameof(HttpRequestHeader.Authorization), ScriptConstants.AdminJwtAppServiceIssuer, ScriptConstants.AdminJwtAppServiceIssuer)]
34+
[InlineData(nameof(HttpRequestHeader.Authorization), "https://appservice.core.azurewebsites.net", "https://testsite.azurewebsites.net")]
35+
[InlineData(nameof(HttpRequestHeader.Authorization), "https://appservice.core.azurewebsites.net", "https://testsite.azurewebsites.net/azurefunctions")]
36+
[InlineData(nameof(HttpRequestHeader.Authorization), "https://testsite.scm.azurewebsites.net", "https://testsite.azurewebsites.net")]
37+
[InlineData(nameof(HttpRequestHeader.Authorization), "https://testsite.scm.azurewebsites.net", "https://testsite.azurewebsites.net/azurefunctions")]
38+
[InlineData(nameof(HttpRequestHeader.Authorization), "https://testsite.azurewebsites.net", "https://testsite.azurewebsites.net")]
3439
[InlineData(ScriptConstants.SiteTokenHeaderName)]
35-
[InlineData(ScriptConstants.SiteTokenHeaderName, ScriptConstants.AdminJwtAppServiceIssuer, ScriptConstants.AdminJwtAppServiceIssuer)]
36-
public async Task InvokeAdminApi_ValidToken_Succeeds(string headerName, string audience = null, string issuer = null)
40+
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://appservice.core.azurewebsites.net", "https://testsite.azurewebsites.net")]
41+
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://appservice.core.azurewebsites.net", "https://testsite.azurewebsites.net/azurefunctions")]
42+
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://testsite.scm.azurewebsites.net", "https://testsite.azurewebsites.net")]
43+
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://testsite.scm.azurewebsites.net", "https://testsite.azurewebsites.net/azurefunctions")]
44+
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://testsite.azurewebsites.net", "https://testsite.azurewebsites.net")]
45+
public async Task InvokeAdminApi_ValidToken_Succeeds(string headerName, string issuer = null, string audience = null)
3746
{
3847
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "admin/host/status");
3948
string token = _fixture.Host.GenerateAdminJwtToken(audience, issuer);
@@ -72,6 +81,18 @@ public async Task InvokeAdminApi_InvalidToken_Fails(string headerName)
7281
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
7382
}
7483

84+
[Fact]
85+
public async Task InvokeAdminApi_ValidToken_UTF8Encoding_Succeeds()
86+
{
87+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "admin/host/status");
88+
string key = SecretsUtility.GetEncryptionKeyValue();
89+
string token = _fixture.Host.GenerateAdminJwtToken(key: Encoding.UTF8.GetBytes(key));
90+
request.Headers.Add(ScriptConstants.SiteTokenHeaderName, token);
91+
92+
var response = await _fixture.Host.HttpClient.SendAsync(request);
93+
response.EnsureSuccessStatusCode();
94+
}
95+
7596
public class TestFixture : EndToEndTestFixture
7697
{
7798
private TestScopedEnvironmentVariable _scopedEnvironment;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Azure.WebJobs.Script.WebHost;
6+
using Xunit;
7+
8+
namespace Microsoft.Azure.WebJobs.Script.Tests.Helpers
9+
{
10+
public class SecretsUtilityTest
11+
{
12+
[Fact]
13+
public void ToKeyBytes_ReturnsExpectedValue()
14+
{
15+
byte[] keyBytes = TestHelpers.GenerateKeyBytes();
16+
17+
string hexKey = TestHelpers.GenerateKeyHexString(keyBytes);
18+
string base64Key = Convert.ToBase64String(keyBytes);
19+
20+
Assert.Equal(keyBytes, SecretsUtility.ToKeyBytes(hexKey));
21+
Assert.Equal(keyBytes, SecretsUtility.ToKeyBytes(base64Key));
22+
Assert.Equal(keyBytes, Convert.FromBase64String(base64Key));
23+
}
24+
}
25+
}

test/WebJobs.Script.Tests/Helpers/SimpleWebTokenTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Security.Cryptography;
65
using Microsoft.AspNetCore.Authentication;
76
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
87
using Xunit;

0 commit comments

Comments
 (0)