Skip to content

Commit 0d4c7fd

Browse files
TonewallTony Choi
andauthored
U/tonychoi/v3 cherry pick (#9546)
* 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]>
1 parent 13c3167 commit 0d4c7fd

File tree

3 files changed

+142
-57
lines changed

3 files changed

+142
-57
lines changed

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

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,51 +26,63 @@ public static class ScriptJwtBearerExtensions
2626
public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilder builder)
2727
=> builder.AddJwtBearer(o =>
2828
{
29-
o.Events = new JwtBearerEvents()
30-
{
31-
OnMessageReceived = c =>
32-
{
33-
// By default, tokens are passed via the standard Authorization Bearer header. However we also support
34-
// passing tokens via the x-ms-site-token header.
35-
if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
36-
{
37-
// the token we set here will be the one used - Authorization header won't be checked.
38-
c.Token = values.FirstOrDefault();
39-
}
40-
41-
// Temporary: Tactical fix to address specialization issues. This should likely be moved to a token validator
42-
// TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
43-
if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
44-
{
45-
o.TokenValidationParameters = CreateTokenValidationParameters();
46-
}
47-
48-
return Task.CompletedTask;
49-
},
50-
OnTokenValidated = c =>
51-
{
52-
c.Principal.AddIdentity(new ClaimsIdentity(new Claim[]
53-
{
54-
new Claim(SecurityConstants.AuthLevelClaimType, AuthorizationLevel.Admin.ToString())
55-
}));
56-
57-
c.Success();
58-
59-
return Task.CompletedTask;
60-
}
61-
};
62-
29+
o.Events = new JwtBearerEvents()
30+
{
31+
OnMessageReceived = c =>
32+
{
33+
// By default, tokens are passed via the standard Authorization Bearer header. However we also support
34+
// passing tokens via the x-ms-site-token header.
35+
if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
36+
{
37+
// the token we set here will be the one used - Authorization header won't be checked.
38+
c.Token = values.FirstOrDefault();
39+
}
40+
// Temporary: Tactical fix to address specialization issues. This should likely be moved to a token validator
41+
// TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
42+
if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
43+
{
6344
o.TokenValidationParameters = CreateTokenValidationParameters();
45+
}
46+
return Task.CompletedTask;
47+
},
48+
OnTokenValidated = c =>
49+
{
50+
c.Principal.AddIdentity(new ClaimsIdentity(new Claim[]
51+
{
52+
new Claim(SecurityConstants.AuthLevelClaimType, AuthorizationLevel.Admin.ToString())
53+
}));
54+
c.Success();
55+
return Task.CompletedTask;
56+
}
57+
};
58+
o.TokenValidationParameters = CreateTokenValidationParameters();
59+
// TODO: DI (FACAVAL) Remove this once the work above is completed.
60+
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
61+
{
62+
// We're not in standby mode, so flag as specialized
63+
_specialized = 1;
64+
}
65+
});
6466

65-
// TODO: DI (FACAVAL) Remove this once the work above is completed.
66-
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
67-
{
68-
// We're not in standby mode, so flag as specialized
69-
_specialized = 1;
70-
}
71-
});
67+
private static string[] GetValidAudiences()
68+
{
69+
if (SystemEnvironment.Instance.IsPlaceholderModeEnabled()
70+
&& SystemEnvironment.Instance.IsLinuxConsumptionOnAtlas())
71+
{
72+
return new string[]
73+
{
74+
ScriptSettingsManager.Instance.GetSetting(ContainerName)
75+
};
76+
}
7277

73-
private static TokenValidationParameters CreateTokenValidationParameters()
78+
return new string[]
79+
{
80+
string.Format(SiteAzureFunctionsUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
81+
string.Format(SiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
82+
};
83+
}
84+
85+
public static TokenValidationParameters CreateTokenValidationParameters()
7486
{
7587
var signingKeys = SecretsUtility.GetTokenIssuerSigningKeys();
7688
var result = new TokenValidationParameters();
@@ -79,11 +91,7 @@ private static TokenValidationParameters CreateTokenValidationParameters()
7991
result.IssuerSigningKeys = signingKeys;
8092
result.ValidateAudience = true;
8193
result.ValidateIssuer = true;
82-
result.ValidAudiences = new string[]
83-
{
84-
string.Format(SiteAzureFunctionsUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
85-
string.Format(SiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
86-
};
94+
result.ValidAudiences = GetValidAudiences();
8795
result.ValidIssuers = new string[]
8896
{
8997
AppServiceCoreUri,

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

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ public static class SimpleWebTokenHelper
1515
/// <summary>
1616
/// A SWT or a Simple Web Token is a token that's made of key=value pairs separated
1717
/// by &. We only specify expiration in ticks from now (exp={ticks})
18-
/// The SWT is then returned as an encrypted string
18+
/// The SWT is then returned as an encrypted string.
1919
/// </summary>
20-
/// <param name="validUntil">Datetime for when the token should expire</param>
21-
/// <param name="key">Optional key to encrypt the token with</param>
22-
/// <returns>a SWT signed by this app</returns>
20+
/// <param name="validUntil">Datetime for when the token should expire.</param>
21+
/// <param name="key">Optional key to encrypt the token with.</param>
22+
/// <returns>a SWT signed by this app.</returns>
2323
public static string CreateToken(DateTime validUntil, byte[] key = null) => Encrypt($"exp={validUntil.Ticks}", key);
2424

25-
internal static string Encrypt(string value, byte[] key = null, IEnvironment environment = null)
25+
internal static string Encrypt(string value, byte[] key = null, IEnvironment environment = null, bool includesSignature = false)
2626
{
2727
key = key ?? SecretsUtility.GetEncryptionKey(environment);
2828

@@ -43,23 +43,31 @@ internal static string Encrypt(string value, byte[] key = null, IEnvironment env
4343
cryptoStream.FlushFinalBlock();
4444
}
4545

46-
// return {iv}.{swt}.{sha236(key)}
47-
return string.Format("{0}.{1}.{2}", iv, Convert.ToBase64String(cipherStream.ToArray()), GetSHA256Base64String(aes.Key));
46+
if (includesSignature)
47+
{
48+
return $"{Convert.ToBase64String(aes.IV)}.{Convert.ToBase64String(cipherStream.ToArray())}.{GetSHA256Base64String(aes.Key)}.{Convert.ToBase64String(ComputeHMACSHA256(aes.Key, input))}";
49+
}
50+
else
51+
{
52+
// return {iv}.{swt}.{sha236(key)}
53+
return string.Format("{0}.{1}.{2}", iv, Convert.ToBase64String(cipherStream.ToArray()), GetSHA256Base64String(aes.Key));
54+
}
4855
}
4956
}
5057
}
5158

5259
public static string Decrypt(byte[] encryptionKey, string value)
5360
{
5461
var parts = value.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
55-
if (parts.Length != 2 && parts.Length != 3)
62+
if (parts.Length != 2 && parts.Length != 3 && parts.Length != 4)
5663
{
5764
throw new InvalidOperationException("Malformed token.");
5865
}
5966

6067
var iv = Convert.FromBase64String(parts[0]);
6168
var data = Convert.FromBase64String(parts[1]);
6269
var base64KeyHash = parts.Length == 3 ? parts[2] : null;
70+
var signature = parts.Length == 4 ? Convert.FromBase64String(parts[3]) : null;
6371

6472
if (!string.IsNullOrEmpty(base64KeyHash) && !string.Equals(GetSHA256Base64String(encryptionKey), base64KeyHash))
6573
{
@@ -76,11 +84,25 @@ public static string Decrypt(byte[] encryptionKey, string value)
7684
binaryWriter.Write(data, 0, data.Length);
7785
}
7886

79-
return Encoding.UTF8.GetString(ms.ToArray());
87+
var input = ms.ToArray();
88+
if (signature != null && !signature.SequenceEqual(ComputeHMACSHA256(encryptionKey, input)))
89+
{
90+
throw new InvalidOperationException("Signature mismatches!");
91+
}
92+
93+
return Encoding.UTF8.GetString(input);
8094
}
8195
}
8296
}
8397

98+
private static byte[] ComputeHMACSHA256(byte[] key, byte[] input)
99+
{
100+
using (var hmacSha256 = new HMACSHA256(key))
101+
{
102+
return hmacSha256.ComputeHash(input);
103+
}
104+
}
105+
84106
public static string Decrypt(string value, IEnvironment environment = null)
85107
{
86108
byte[] key = SecretsUtility.GetEncryptionKey(environment);

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +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.Security.Cryptography;
5+
using System.Collections.Generic;
66
using Microsoft.AspNetCore.Authentication;
7+
using Microsoft.Azure.WebJobs.Script.WebHost;
8+
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
79
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
10+
using Newtonsoft.Json;
811
using Xunit;
912

1013
namespace Microsoft.Azure.WebJobs.Script.Tests.Helpers
@@ -86,11 +89,63 @@ public void Validate_Token_Uses_Website_Encryption_Key_If_Container_Encryption_K
8689
Assert.True(SimpleWebTokenHelper.TryValidateToken(token, new SystemClock()));
8790
}
8891

92+
[Fact]
93+
public void Validate_Token_Checks_Signature_If_Signature_Is_Available()
94+
{
95+
var websiteAuthEncryptionKey = TestHelpers.GenerateKeyBytes();
96+
var websiteAuthEncryptionStringKey = TestHelpers.GenerateKeyHexString(websiteAuthEncryptionKey);
97+
98+
var timeStamp = DateTime.UtcNow.AddHours(1);
99+
100+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
101+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, websiteAuthEncryptionStringKey);
102+
103+
var token = SimpleWebTokenHelper.Encrypt($"exp={timeStamp.Ticks}", websiteAuthEncryptionKey, includesSignature: true);
104+
105+
Assert.True(SimpleWebTokenHelper.TryValidateToken(token, new SystemClock()));
106+
}
107+
108+
[Fact]
109+
public void Encrypt_And_Decrypt_Context_With_Signature()
110+
{
111+
var websiteAuthEncryptionKey = TestHelpers.GenerateKeyBytes();
112+
var websiteAuthEncryptionStringKey = TestHelpers.GenerateKeyHexString(websiteAuthEncryptionKey);
113+
var hostContext = GetHostAssignmentContext();
114+
var hostContextJson = JsonConvert.SerializeObject(hostContext);
115+
116+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
117+
Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, websiteAuthEncryptionStringKey);
118+
119+
var encryptedHostContextWithSignature = SimpleWebTokenHelper.Encrypt(hostContextJson, websiteAuthEncryptionKey, includesSignature: true);
120+
121+
var decryptedHostContextJson = SimpleWebTokenHelper.Decrypt(websiteAuthEncryptionKey, encryptedHostContextWithSignature);
122+
123+
Assert.Equal(hostContextJson, decryptedHostContextJson);
124+
}
125+
89126
public void Dispose()
90127
{
91128
// Clean up
92129
Environment.SetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, string.Empty);
93130
Environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, string.Empty);
94131
}
132+
133+
private static HostAssignmentContext GetHostAssignmentContext()
134+
{
135+
var hostAssignmentContext = new HostAssignmentContext();
136+
hostAssignmentContext.SiteId = 1;
137+
hostAssignmentContext.SiteName = "sitename";
138+
hostAssignmentContext.LastModifiedTime = DateTime.UtcNow.Add(TimeSpan.FromMinutes(new Random().Next()));
139+
hostAssignmentContext.Environment = new Dictionary<string, string>();
140+
hostAssignmentContext.MSIContext = new MSIContext();
141+
hostAssignmentContext.EncryptedTokenServiceSpecializationPayload = "payload";
142+
hostAssignmentContext.TokenServiceApiEndpoint = "endpoints";
143+
hostAssignmentContext.CorsSettings = new CorsSettings();
144+
hostAssignmentContext.EasyAuthSettings = new EasyAuthSettings();
145+
hostAssignmentContext.Secrets = new FunctionAppSecrets();
146+
hostAssignmentContext.IsWarmupRequest = false;
147+
148+
return hostAssignmentContext;
149+
}
95150
}
96151
}

0 commit comments

Comments
 (0)