Skip to content

Commit 2ae1292

Browse files
authored
Making JWT token audience issuer validation case insensitive. (#9678)
1 parent f2fb43a commit 2ae1292

File tree

3 files changed

+128
-4
lines changed

3 files changed

+128
-4
lines changed

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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;
45
using System.Collections.Generic;
56
using System.Linq;
67
using System.Security.Claims;
@@ -13,6 +14,7 @@
1314
using Microsoft.Azure.WebJobs.Script.Config;
1415
using Microsoft.Azure.WebJobs.Script.WebHost;
1516
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication;
17+
using Microsoft.Extensions.Logging;
1618
using Microsoft.Extensions.Primitives;
1719
using Microsoft.IdentityModel.Tokens;
1820
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
@@ -57,6 +59,12 @@ public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilde
5759

5860
c.Success();
5961

62+
return Task.CompletedTask;
63+
},
64+
OnAuthenticationFailed = c =>
65+
{
66+
LogAuthenticationFailure(c);
67+
6068
return Task.CompletedTask;
6169
}
6270
};
@@ -105,8 +113,8 @@ public static TokenValidationParameters CreateTokenValidationParameters()
105113
if (signingKeys.Length > 0)
106114
{
107115
result.IssuerSigningKeys = signingKeys;
108-
result.ValidateAudience = true;
109-
result.ValidateIssuer = true;
116+
result.AudienceValidator = AudienceValidator;
117+
result.IssuerValidator = IssuerValidator;
110118
result.ValidAudiences = GetValidAudiences();
111119
result.ValidIssuers = new string[]
112120
{
@@ -118,5 +126,53 @@ public static TokenValidationParameters CreateTokenValidationParameters()
118126

119127
return result;
120128
}
129+
130+
private static string IssuerValidator(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
131+
{
132+
if (!validationParameters.ValidIssuers.Any(p => string.Equals(issuer, p, StringComparison.OrdinalIgnoreCase)))
133+
{
134+
throw new SecurityTokenInvalidIssuerException("IDX10205: Issuer validation failed.")
135+
{
136+
InvalidIssuer = issuer,
137+
};
138+
}
139+
140+
return issuer;
141+
}
142+
143+
private static bool AudienceValidator(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
144+
{
145+
foreach (string audience in audiences)
146+
{
147+
if (validationParameters.ValidAudiences.Any(p => string.Equals(audience, p, StringComparison.OrdinalIgnoreCase)))
148+
{
149+
return true;
150+
}
151+
}
152+
153+
return false;
154+
}
155+
156+
private static void LogAuthenticationFailure(AuthenticationFailedContext context)
157+
{
158+
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
159+
var logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryHostAuthentication);
160+
161+
string message = null;
162+
switch (context.Exception)
163+
{
164+
case SecurityTokenInvalidIssuerException iex:
165+
message = $"Token issuer validation failed for issuer '{iex.InvalidIssuer}'.";
166+
break;
167+
case SecurityTokenInvalidAudienceException iaex:
168+
message = $"Token audience validation failed for audience '{iaex.InvalidAudience}'.";
169+
break;
170+
default:
171+
message = $"Token validation failed.";
172+
break;
173+
}
174+
175+
logger.LogError(context.Exception, message);
176+
}
121177
}
122178
}

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public static class ScriptConstants
5151
public const string LogCategoryFunctionsController = "Host.Controllers.Functions";
5252
public const string LogCategoryInstanceController = "Host.Controllers.Instance";
5353
public const string LogCategoryKeysController = "Host.Controllers.Keys";
54+
public const string LogCategoryHostAuthentication = "Host.Authentication";
5455
public const string LogCategoryHostGeneral = "Host.General";
5556
public const string LogCategoryHostMetrics = "Host.Metrics";
5657
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)