Skip to content

Commit

Permalink
U/tonychoi/v3 cherry pick (#9546)
Browse files Browse the repository at this point in the history
* Update valid audience for legion applications during specialization

* Update valid audience for legion applications during specialization P2

* fix inden

* Use auth to decrypt signature

* missed commit

---------

Co-authored-by: Tony Choi <[email protected]>
  • Loading branch information
Tonewall and Tony Choi authored Sep 21, 2023
1 parent 13c3167 commit 0d4c7fd
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down
40 changes: 31 additions & 9 deletions src/WebJobs.Script.WebHost/Security/SimpleWebTokenHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ public static class SimpleWebTokenHelper
/// <summary>
/// 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.
/// </summary>
/// <param name="validUntil">Datetime for when the token should expire</param>
/// <param name="key">Optional key to encrypt the token with</param>
/// <returns>a SWT signed by this app</returns>
/// <param name="validUntil">Datetime for when the token should expire.</param>
/// <param name="key">Optional key to encrypt the token with.</param>
/// <returns>a SWT signed by this app.</returns>
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);

Expand All @@ -43,23 +43,31 @@ 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));
}
}
}
}

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.");
}

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))
{
Expand All @@ -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);
Expand Down
57 changes: 56 additions & 1 deletion test/WebJobs.Script.Tests/Helpers/SimpleWebTokenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string, string>();
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;
}
}
}

0 comments on commit 0d4c7fd

Please sign in to comment.