diff --git a/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs b/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs
index 6dde506446..c688aa2bb1 100644
--- a/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs
+++ b/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs
@@ -26,51 +26,63 @@ public static class ScriptJwtBearerExtensions
public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilder builder)
=> builder.AddJwtBearer(o =>
{
- o.Events = new JwtBearerEvents()
- {
- OnMessageReceived = c =>
- {
- // By default, tokens are passed via the standard Authorization Bearer header. However we also support
- // passing tokens via the x-ms-site-token header.
- if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
- {
- // the token we set here will be the one used - Authorization header won't be checked.
- c.Token = values.FirstOrDefault();
- }
-
- // Temporary: Tactical fix to address specialization issues. This should likely be moved to a token validator
- // TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
- if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
- {
- o.TokenValidationParameters = CreateTokenValidationParameters();
- }
-
- return Task.CompletedTask;
- },
- OnTokenValidated = c =>
- {
- c.Principal.AddIdentity(new ClaimsIdentity(new Claim[]
- {
- new Claim(SecurityConstants.AuthLevelClaimType, AuthorizationLevel.Admin.ToString())
- }));
-
- c.Success();
-
- return Task.CompletedTask;
- }
- };
-
+ o.Events = new JwtBearerEvents()
+ {
+ OnMessageReceived = c =>
+ {
+ // By default, tokens are passed via the standard Authorization Bearer header. However we also support
+ // passing tokens via the x-ms-site-token header.
+ if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
+ {
+ // the token we set here will be the one used - Authorization header won't be checked.
+ c.Token = values.FirstOrDefault();
+ }
+ // Temporary: Tactical fix to address specialization issues. This should likely be moved to a token validator
+ // TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
+ if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
+ {
o.TokenValidationParameters = CreateTokenValidationParameters();
+ }
+ return Task.CompletedTask;
+ },
+ OnTokenValidated = c =>
+ {
+ c.Principal.AddIdentity(new ClaimsIdentity(new Claim[]
+ {
+ new Claim(SecurityConstants.AuthLevelClaimType, AuthorizationLevel.Admin.ToString())
+ }));
+ c.Success();
+ return Task.CompletedTask;
+ }
+ };
+ o.TokenValidationParameters = CreateTokenValidationParameters();
+ // TODO: DI (FACAVAL) Remove this once the work above is completed.
+ if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
+ {
+ // We're not in standby mode, so flag as specialized
+ _specialized = 1;
+ }
+ });
- // TODO: DI (FACAVAL) Remove this once the work above is completed.
- if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
- {
- // We're not in standby mode, so flag as specialized
- _specialized = 1;
- }
- });
+ private static string[] GetValidAudiences()
+ {
+ if (SystemEnvironment.Instance.IsPlaceholderModeEnabled()
+ && SystemEnvironment.Instance.IsLinuxConsumptionOnAtlas())
+ {
+ return new string[]
+ {
+ ScriptSettingsManager.Instance.GetSetting(ContainerName)
+ };
+ }
- private static TokenValidationParameters CreateTokenValidationParameters()
+ return new string[]
+ {
+ string.Format(SiteAzureFunctionsUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
+ string.Format(SiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
+ };
+ }
+
+ public static TokenValidationParameters CreateTokenValidationParameters()
{
var signingKeys = SecretsUtility.GetTokenIssuerSigningKeys();
var result = new TokenValidationParameters();
@@ -79,11 +91,7 @@ private static TokenValidationParameters CreateTokenValidationParameters()
result.IssuerSigningKeys = signingKeys;
result.ValidateAudience = true;
result.ValidateIssuer = true;
- result.ValidAudiences = new string[]
- {
- string.Format(SiteAzureFunctionsUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
- string.Format(SiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
- };
+ result.ValidAudiences = GetValidAudiences();
result.ValidIssuers = new string[]
{
AppServiceCoreUri,
diff --git a/src/WebJobs.Script.WebHost/Security/SimpleWebTokenHelper.cs b/src/WebJobs.Script.WebHost/Security/SimpleWebTokenHelper.cs
index ff34c310f0..f62d1fa174 100644
--- a/src/WebJobs.Script.WebHost/Security/SimpleWebTokenHelper.cs
+++ b/src/WebJobs.Script.WebHost/Security/SimpleWebTokenHelper.cs
@@ -15,14 +15,14 @@ public static class SimpleWebTokenHelper
///
/// A SWT or a Simple Web Token is a token that's made of key=value pairs separated
/// by &. We only specify expiration in ticks from now (exp={ticks})
- /// The SWT is then returned as an encrypted string
+ /// The SWT is then returned as an encrypted string.
///
- /// Datetime for when the token should expire
- /// Optional key to encrypt the token with
- /// a SWT signed by this app
+ /// Datetime for when the token should expire.
+ /// Optional key to encrypt the token with.
+ /// a SWT signed by this app.
public static string CreateToken(DateTime validUntil, byte[] key = null) => Encrypt($"exp={validUntil.Ticks}", key);
- internal static string Encrypt(string value, byte[] key = null, IEnvironment environment = null)
+ internal static string Encrypt(string value, byte[] key = null, IEnvironment environment = null, bool includesSignature = false)
{
key = key ?? SecretsUtility.GetEncryptionKey(environment);
@@ -43,8 +43,15 @@ internal static string Encrypt(string value, byte[] key = null, IEnvironment env
cryptoStream.FlushFinalBlock();
}
- // return {iv}.{swt}.{sha236(key)}
- return string.Format("{0}.{1}.{2}", iv, Convert.ToBase64String(cipherStream.ToArray()), GetSHA256Base64String(aes.Key));
+ if (includesSignature)
+ {
+ return $"{Convert.ToBase64String(aes.IV)}.{Convert.ToBase64String(cipherStream.ToArray())}.{GetSHA256Base64String(aes.Key)}.{Convert.ToBase64String(ComputeHMACSHA256(aes.Key, input))}";
+ }
+ else
+ {
+ // return {iv}.{swt}.{sha236(key)}
+ return string.Format("{0}.{1}.{2}", iv, Convert.ToBase64String(cipherStream.ToArray()), GetSHA256Base64String(aes.Key));
+ }
}
}
}
@@ -52,7 +59,7 @@ internal static string Encrypt(string value, byte[] key = null, IEnvironment env
public static string Decrypt(byte[] encryptionKey, string value)
{
var parts = value.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
- if (parts.Length != 2 && parts.Length != 3)
+ if (parts.Length != 2 && parts.Length != 3 && parts.Length != 4)
{
throw new InvalidOperationException("Malformed token.");
}
@@ -60,6 +67,7 @@ public static string Decrypt(byte[] encryptionKey, string value)
var iv = Convert.FromBase64String(parts[0]);
var data = Convert.FromBase64String(parts[1]);
var base64KeyHash = parts.Length == 3 ? parts[2] : null;
+ var signature = parts.Length == 4 ? Convert.FromBase64String(parts[3]) : null;
if (!string.IsNullOrEmpty(base64KeyHash) && !string.Equals(GetSHA256Base64String(encryptionKey), base64KeyHash))
{
@@ -76,11 +84,25 @@ public static string Decrypt(byte[] encryptionKey, string value)
binaryWriter.Write(data, 0, data.Length);
}
- return Encoding.UTF8.GetString(ms.ToArray());
+ var input = ms.ToArray();
+ if (signature != null && !signature.SequenceEqual(ComputeHMACSHA256(encryptionKey, input)))
+ {
+ throw new InvalidOperationException("Signature mismatches!");
+ }
+
+ return Encoding.UTF8.GetString(input);
}
}
}
+ private static byte[] ComputeHMACSHA256(byte[] key, byte[] input)
+ {
+ using (var hmacSha256 = new HMACSHA256(key))
+ {
+ return hmacSha256.ComputeHash(input);
+ }
+ }
+
public static string Decrypt(string value, IEnvironment environment = null)
{
byte[] key = SecretsUtility.GetEncryptionKey(environment);
diff --git a/test/WebJobs.Script.Tests/Helpers/SimpleWebTokenTests.cs b/test/WebJobs.Script.Tests/Helpers/SimpleWebTokenTests.cs
index d12321ec39..5f9bfefc83 100644
--- a/test/WebJobs.Script.Tests/Helpers/SimpleWebTokenTests.cs
+++ b/test/WebJobs.Script.Tests/Helpers/SimpleWebTokenTests.cs
@@ -2,9 +2,12 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
-using System.Security.Cryptography;
+using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
+using Microsoft.Azure.WebJobs.Script.WebHost;
+using Microsoft.Azure.WebJobs.Script.WebHost.Models;
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
+using Newtonsoft.Json;
using Xunit;
namespace Microsoft.Azure.WebJobs.Script.Tests.Helpers
@@ -86,11 +89,63 @@ public void Validate_Token_Uses_Website_Encryption_Key_If_Container_Encryption_K
Assert.True(SimpleWebTokenHelper.TryValidateToken(token, new SystemClock()));
}
+ [Fact]
+ public void Validate_Token_Checks_Signature_If_Signature_Is_Available()
+ {
+ var websiteAuthEncryptionKey = TestHelpers.GenerateKeyBytes();
+ var websiteAuthEncryptionStringKey = TestHelpers.GenerateKeyHexString(websiteAuthEncryptionKey);
+
+ var timeStamp = DateTime.UtcNow.AddHours(1);
+
+ Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
+ Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, websiteAuthEncryptionStringKey);
+
+ var token = SimpleWebTokenHelper.Encrypt($"exp={timeStamp.Ticks}", websiteAuthEncryptionKey, includesSignature: true);
+
+ Assert.True(SimpleWebTokenHelper.TryValidateToken(token, new SystemClock()));
+ }
+
+ [Fact]
+ public void Encrypt_And_Decrypt_Context_With_Signature()
+ {
+ var websiteAuthEncryptionKey = TestHelpers.GenerateKeyBytes();
+ var websiteAuthEncryptionStringKey = TestHelpers.GenerateKeyHexString(websiteAuthEncryptionKey);
+ var hostContext = GetHostAssignmentContext();
+ var hostContextJson = JsonConvert.SerializeObject(hostContext);
+
+ Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
+ Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, websiteAuthEncryptionStringKey);
+
+ var encryptedHostContextWithSignature = SimpleWebTokenHelper.Encrypt(hostContextJson, websiteAuthEncryptionKey, includesSignature: true);
+
+ var decryptedHostContextJson = SimpleWebTokenHelper.Decrypt(websiteAuthEncryptionKey, encryptedHostContextWithSignature);
+
+ Assert.Equal(hostContextJson, decryptedHostContextJson);
+ }
+
public void Dispose()
{
// Clean up
Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, string.Empty);
Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
}
+
+ private static HostAssignmentContext GetHostAssignmentContext()
+ {
+ var hostAssignmentContext = new HostAssignmentContext();
+ hostAssignmentContext.SiteId = 1;
+ hostAssignmentContext.SiteName = "sitename";
+ hostAssignmentContext.LastModifiedTime = DateTime.UtcNow.Add(TimeSpan.FromMinutes(new Random().Next()));
+ hostAssignmentContext.Environment = new Dictionary();
+ hostAssignmentContext.MSIContext = new MSIContext();
+ hostAssignmentContext.EncryptedTokenServiceSpecializationPayload = "payload";
+ hostAssignmentContext.TokenServiceApiEndpoint = "endpoints";
+ hostAssignmentContext.CorsSettings = new CorsSettings();
+ hostAssignmentContext.EasyAuthSettings = new EasyAuthSettings();
+ hostAssignmentContext.Secrets = new FunctionAppSecrets();
+ hostAssignmentContext.IsWarmupRequest = false;
+
+ return hostAssignmentContext;
+ }
}
}