Skip to content

Commit 3e4dcad

Browse files
authored
Making JWT token audience/issuer validation case insensitive. (#9681)
1 parent b7aeec9 commit 3e4dcad

File tree

3 files changed

+129
-4
lines changed

3 files changed

+129
-4
lines changed

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

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Collections.Generic;
46
using System.Linq;
57
using System.Security.Claims;
68
using System.Threading;
@@ -12,6 +14,7 @@
1214
using Microsoft.Azure.WebJobs.Script.Config;
1315
using Microsoft.Azure.WebJobs.Script.WebHost;
1416
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication;
17+
using Microsoft.Extensions.Logging;
1518
using Microsoft.Extensions.Primitives;
1619
using Microsoft.IdentityModel.Tokens;
1720
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
@@ -56,6 +59,12 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
5659

5760
c.Success();
5861

62+
return Task.CompletedTask;
63+
},
64+
OnAuthenticationFailed = c =>
65+
{
66+
LogAuthenticationFailure(c);
67+
5968
return Task.CompletedTask;
6069
}
6170
};
@@ -77,8 +86,8 @@ private static TokenValidationParameters CreateTokenValidationParameters()
7786
if (signingKeys.Length > 0)
7887
{
7988
result.IssuerSigningKeys = signingKeys;
80-
result.ValidateAudience = true;
81-
result.ValidateIssuer = true;
89+
result.AudienceValidator = AudienceValidator;
90+
result.IssuerValidator = IssuerValidator;
8291
result.ValidAudiences = new string[]
8392
{
8493
string.Format(SiteAzureFunctionsUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
@@ -94,5 +103,53 @@ private static TokenValidationParameters CreateTokenValidationParameters()
94103

95104
return result;
96105
}
106+
107+
private static string IssuerValidator(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
108+
{
109+
if (!validationParameters.ValidIssuers.Any(p => string.Equals(issuer, p, StringComparison.OrdinalIgnoreCase)))
110+
{
111+
throw new SecurityTokenInvalidIssuerException("IDX10205: Issuer validation failed.")
112+
{
113+
InvalidIssuer = issuer,
114+
};
115+
}
116+
117+
return issuer;
118+
}
119+
120+
private static bool AudienceValidator(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
121+
{
122+
foreach (string audience in audiences)
123+
{
124+
if (validationParameters.ValidAudiences.Any(p => string.Equals(audience, p, StringComparison.OrdinalIgnoreCase)))
125+
{
126+
return true;
127+
}
128+
}
129+
130+
return false;
131+
}
132+
133+
private static void LogAuthenticationFailure(AuthenticationFailedContext context)
134+
{
135+
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
136+
var logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryHostAuthentication);
137+
138+
string message = null;
139+
switch (context.Exception)
140+
{
141+
case SecurityTokenInvalidIssuerException iex:
142+
message = $"Token issuer validation failed for issuer '{iex.InvalidIssuer}'.";
143+
break;
144+
case SecurityTokenInvalidAudienceException iaex:
145+
message = $"Token audience validation failed for audience '{iaex.InvalidAudience}'.";
146+
break;
147+
default:
148+
message = $"Token validation failed.";
149+
break;
150+
}
151+
152+
logger.LogError(context.Exception, message);
153+
}
97154
}
98155
}

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public static class ScriptConstants
5050
public const string LogCategoryFunctionsController = "Host.Controllers.Functions";
5151
public const string LogCategoryInstanceController = "Host.Controllers.Instance";
5252
public const string LogCategoryKeysController = "Host.Controllers.Keys";
53+
public const string LogCategoryHostAuthentication = "Host.Authentication";
5354
public const string LogCategoryHostGeneral = "Host.General";
5455
public const string LogCategoryHostMetrics = "Host.Metrics";
5556
public const string LogCategoryHost = "Host";

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

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
1414
using Microsoft.Extensions.DependencyInjection;
1515
using Microsoft.Extensions.DependencyInjection.Extensions;
16+
using Microsoft.Extensions.Logging;
1617
using Xunit;
1718

1819
namespace Microsoft.Azure.WebJobs.Script.Tests.Integration.WebHostEndToEnd
@@ -38,6 +39,7 @@ public JwtTokenAuthTests(TestFixture fixture)
3839
[InlineData(nameof(HttpRequestHeader.Authorization), "https://testsite.azurewebsites.net", "https://testsite.azurewebsites.net")]
3940
[InlineData(ScriptConstants.SiteTokenHeaderName)]
4041
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://appservice.core.azurewebsites.net", "https://testsite.azurewebsites.net")]
42+
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://AppService.Core.Azurewebsites.net", "https://TestSite.Azurewebsites.net")]
4143
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://appservice.core.azurewebsites.net", "https://testsite.azurewebsites.net/azurefunctions")]
4244
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://testsite.scm.azurewebsites.net", "https://testsite.azurewebsites.net")]
4345
[InlineData(ScriptConstants.SiteTokenHeaderName, "https://testsite.scm.azurewebsites.net", "https://testsite.azurewebsites.net/azurefunctions")]
@@ -63,10 +65,10 @@ public async Task InvokeAdminApi_ValidToken_Succeeds(string headerName, string i
6365
[Theory]
6466
[InlineData(nameof(HttpRequestHeader.Authorization))]
6567
[InlineData(ScriptConstants.SiteTokenHeaderName)]
66-
public async Task InvokeAdminApi_InvalidToken_Fails(string headerName)
68+
public async Task InvokeAdminApi_InvalidAudience_Fails(string headerName)
6769
{
6870
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "admin/host/status");
69-
string token = _fixture.Host.GenerateAdminJwtToken("invalid", "invalid");
71+
string token = _fixture.Host.GenerateAdminJwtToken(audience: "invalid");
7072

7173
if (string.Compare(nameof(HttpRequestHeader.Authorization), headerName) == 0)
7274
{
@@ -77,8 +79,73 @@ public async Task InvokeAdminApi_InvalidToken_Fails(string headerName)
7779
request.Headers.Add(headerName, token);
7880
}
7981

82+
_fixture.Host.ClearLogMessages();
83+
8084
var response = await _fixture.Host.HttpClient.SendAsync(request);
8185
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
86+
87+
var validationError = _fixture.Host.GetScriptHostLogMessages().Single(p => p.Level == LogLevel.Error);
88+
Assert.Equal(ScriptConstants.LogCategoryHostAuthentication, validationError.Category);
89+
Assert.Equal("Token audience validation failed for audience 'invalid'.", validationError.FormattedMessage);
90+
Assert.True(validationError.Exception.Message.StartsWith("IDX10231: Audience validation failed."));
91+
}
92+
93+
[Theory]
94+
[InlineData(nameof(HttpRequestHeader.Authorization))]
95+
[InlineData(ScriptConstants.SiteTokenHeaderName)]
96+
public async Task InvokeAdminApi_InvalidIssuer_Fails(string headerName)
97+
{
98+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "admin/host/status");
99+
string token = _fixture.Host.GenerateAdminJwtToken(issuer: "invalid");
100+
101+
if (string.Compare(nameof(HttpRequestHeader.Authorization), headerName) == 0)
102+
{
103+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
104+
}
105+
else
106+
{
107+
request.Headers.Add(headerName, token);
108+
}
109+
110+
_fixture.Host.ClearLogMessages();
111+
112+
var response = await _fixture.Host.HttpClient.SendAsync(request);
113+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
114+
115+
var validationError = _fixture.Host.GetScriptHostLogMessages().Single(p => p.Level == LogLevel.Error);
116+
Assert.Equal(ScriptConstants.LogCategoryHostAuthentication, validationError.Category);
117+
Assert.Equal("Token issuer validation failed for issuer 'invalid'.", validationError.FormattedMessage);
118+
Assert.Equal("IDX10205: Issuer validation failed.", validationError.Exception.Message);
119+
}
120+
121+
[Theory]
122+
[InlineData(nameof(HttpRequestHeader.Authorization))]
123+
[InlineData(ScriptConstants.SiteTokenHeaderName)]
124+
public async Task InvokeAdminApi_InvalidSignature_Fails(string headerName)
125+
{
126+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "admin/host/status");
127+
128+
byte[] keyBytes = TestHelpers.GenerateKeyBytes();
129+
string token = _fixture.Host.GenerateAdminJwtToken(key: keyBytes);
130+
131+
if (string.Compare(nameof(HttpRequestHeader.Authorization), headerName) == 0)
132+
{
133+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
134+
}
135+
else
136+
{
137+
request.Headers.Add(headerName, token);
138+
}
139+
140+
_fixture.Host.ClearLogMessages();
141+
142+
var response = await _fixture.Host.HttpClient.SendAsync(request);
143+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
144+
145+
var validationError = _fixture.Host.GetScriptHostLogMessages().Single(p => p.Level == LogLevel.Error);
146+
Assert.Equal(ScriptConstants.LogCategoryHostAuthentication, validationError.Category);
147+
Assert.Equal("Token validation failed.", validationError.FormattedMessage);
148+
Assert.True(validationError.Exception.Message.StartsWith("IDX10503: Signature validation failed."));
82149
}
83150

84151
[Fact]

0 commit comments

Comments
 (0)