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; + } } }