Skip to content

Commit da46c0e

Browse files
authored
Scope ScriptJwtBearerHandler logs to system-only (#10617)
* Return NoResult for non-admin APIs for script JWT auth * Return NoResult for invalid issuers for JWT auth * Pass NullLogger to JwtBearerHandler * Introduce new ISystemLoggerFactory for system-only loggers * Fix e2e tests * Default to NullLoggerFactory to address tests * update release_notes.md
1 parent f8b16d4 commit da46c0e

File tree

9 files changed

+142
-53
lines changed

9 files changed

+142
-53
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
- My change description (#PR)
55
-->
66
- Introduced proper handling in environments where .NET in-proc is not supported.
7+
- Suppress `JwtBearerHandler` logs from customer logs (#10617)
78
- Updated System.Memory.Data reference to 8.0.1
89
- Address issue with HTTP proxying throwing `ArgumentException` (#10616)

src/WebJobs.Script.WebHost/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.Builder;
88
using Microsoft.AspNetCore.Hosting;
99
using Microsoft.Azure.WebJobs.Script.Config;
10+
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1011
using Microsoft.Azure.WebJobs.Script.WebHost.Configuration;
1112
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
1213
using Microsoft.Extensions.Configuration;
@@ -89,7 +90,7 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args = null)
8990
loggingBuilder.AddWebJobsSystem<WebHostSystemLoggerProvider>();
9091
loggingBuilder.Services.AddSingleton<DeferredLoggerProvider>();
9192
loggingBuilder.Services.AddSingleton<ILoggerProvider>(s => s.GetRequiredService<DeferredLoggerProvider>());
92-
93+
loggingBuilder.Services.AddSingleton<ISystemLoggerFactory, SystemLoggerFactory>();
9394
if (context.HostingEnvironment.IsDevelopment())
9495
{
9596
loggingBuilder.AddConsole();

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

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
using Microsoft.Azure.WebJobs.Script.Extensions;
1616
using Microsoft.Azure.WebJobs.Script.WebHost;
1717
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication;
18+
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication.Jwt;
19+
using Microsoft.Extensions.DependencyInjection.Extensions;
1820
using Microsoft.Extensions.Logging;
21+
using Microsoft.Extensions.Options;
1922
using Microsoft.Extensions.Primitives;
2023
using Microsoft.IdentityModel.Tokens;
2124
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
@@ -28,63 +31,68 @@ public static class ScriptJwtBearerExtensions
2831
private static double _specialized = 0;
2932

3033
public static AuthenticationBuilder AddScriptJwtBearer(this AuthenticationBuilder builder)
31-
=> builder.AddJwtBearer(o =>
34+
{
35+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
36+
return builder.AddScheme<JwtBearerOptions, ScriptJwtBearerHandler>(
37+
JwtBearerDefaults.AuthenticationScheme, displayName: null, ConfigureOptions);
38+
}
39+
40+
private static void ConfigureOptions(JwtBearerOptions options)
41+
{
42+
options.Events = new JwtBearerEvents()
3243
{
33-
o.Events = new JwtBearerEvents()
44+
OnMessageReceived = c =>
3445
{
35-
OnMessageReceived = c =>
46+
// By default, tokens are passed via the standard Authorization Bearer header. However we also support
47+
// passing tokens via the x-ms-site-token header.
48+
if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
3649
{
37-
// By default, tokens are passed via the standard Authorization Bearer header. However we also support
38-
// passing tokens via the x-ms-site-token header.
39-
if (c.Request.Headers.TryGetValue(ScriptConstants.SiteTokenHeaderName, out StringValues values))
40-
{
41-
// the token we set here will be the one used - Authorization header won't be checked.
42-
c.Token = values.FirstOrDefault();
43-
}
44-
45-
// Temporary: Tactical fix to address specialization issues. This should likely be moved to a token validator
46-
// TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
47-
if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
48-
{
49-
o.TokenValidationParameters = CreateTokenValidationParameters();
50-
}
51-
52-
return Task.CompletedTask;
53-
},
54-
OnTokenValidated = c =>
50+
// the token we set here will be the one used - Authorization header won't be checked.
51+
c.Token = values.FirstOrDefault();
52+
}
53+
54+
// Temporary: Tactical fix to address specialization issues. This should likely be moved to a token validator
55+
// TODO: DI (FACAVAL) This will be fixed once the permanent fix is in place
56+
if (_specialized == 0 && !SystemEnvironment.Instance.IsPlaceholderModeEnabled() && Interlocked.CompareExchange(ref _specialized, 1, 0) == 0)
5557
{
56-
var claims = new List<Claim>
57-
{
58-
new Claim(SecurityConstants.AuthLevelClaimType, AuthorizationLevel.Admin.ToString())
59-
};
60-
if (!string.Equals(c.SecurityToken.Issuer, ScriptConstants.AppServiceCoreUri, StringComparison.OrdinalIgnoreCase))
61-
{
62-
claims.Add(new Claim(SecurityConstants.InvokeClaimType, "true"));
63-
}
64-
65-
c.Principal.AddIdentity(new ClaimsIdentity(claims));
66-
67-
c.Success();
68-
69-
return Task.CompletedTask;
70-
},
71-
OnAuthenticationFailed = c =>
58+
options.TokenValidationParameters = CreateTokenValidationParameters();
59+
}
60+
61+
return Task.CompletedTask;
62+
},
63+
OnTokenValidated = c =>
64+
{
65+
var claims = new List<Claim>
7266
{
73-
LogAuthenticationFailure(c);
67+
new Claim(SecurityConstants.AuthLevelClaimType, AuthorizationLevel.Admin.ToString())
68+
};
7469

75-
return Task.CompletedTask;
70+
if (!string.Equals(c.SecurityToken.Issuer, ScriptConstants.AppServiceCoreUri, StringComparison.OrdinalIgnoreCase))
71+
{
72+
claims.Add(new Claim(SecurityConstants.InvokeClaimType, "true"));
7673
}
77-
};
7874

79-
o.TokenValidationParameters = CreateTokenValidationParameters();
75+
c.Principal.AddIdentity(new ClaimsIdentity(claims));
76+
c.Success();
8077

81-
// TODO: DI (FACAVAL) Remove this once the work above is completed.
82-
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
78+
return Task.CompletedTask;
79+
},
80+
OnAuthenticationFailed = c =>
8381
{
84-
// We're not in standby mode, so flag as specialized
85-
_specialized = 1;
82+
LogAuthenticationFailure(c);
83+
return Task.CompletedTask;
8684
}
87-
});
85+
};
86+
87+
options.TokenValidationParameters = CreateTokenValidationParameters();
88+
89+
// TODO: DI (FACAVAL) Remove this once the work above is completed.
90+
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
91+
{
92+
// We're not in standby mode, so flag as specialized
93+
_specialized = 1;
94+
}
95+
}
8896

8997
private static IEnumerable<string> GetValidAudiences()
9098
{
@@ -134,12 +142,12 @@ public static TokenValidationParameters CreateTokenValidationParameters()
134142
result.AudienceValidator = AudienceValidator;
135143
result.IssuerValidator = IssuerValidator;
136144
result.ValidAudiences = GetValidAudiences();
137-
result.ValidIssuers = new string[]
138-
{
145+
result.ValidIssuers =
146+
[
139147
AppServiceCoreUri,
140148
string.Format(ScmSiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)),
141149
string.Format(SiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName))
142-
};
150+
];
143151
}
144152

145153
return result;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Text.Encodings.Web;
5+
using Microsoft.AspNetCore.Authentication;
6+
using Microsoft.AspNetCore.Authentication.JwtBearer;
7+
using Microsoft.Azure.WebJobs.Script.Diagnostics;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Logging.Abstractions;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication.Jwt
13+
{
14+
internal sealed class ScriptJwtBearerHandler : JwtBearerHandler
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ScriptJwtBearerHandler"/> class.
18+
/// </summary>
19+
/// <param name="options">The options.</param>
20+
/// <param name="encoder">The url encoder.</param>
21+
/// <param name="clock">The system clock.</param>
22+
/// <param name="loggerFactory">The system logger factory.</param>
23+
public ScriptJwtBearerHandler(
24+
IOptionsMonitor<JwtBearerOptions> options,
25+
UrlEncoder encoder,
26+
ISystemClock clock,
27+
ISystemLoggerFactory loggerFactory = null)
28+
: base(options, (ILoggerFactory)loggerFactory ?? NullLoggerFactory.Instance, encoder, clock)
29+
{
30+
// Note - ISystemLoggerFactory falls back to NullLoggerFactory to avoid needing this service in tests.
31+
}
32+
}
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Diagnostics
7+
{
8+
/// <summary>
9+
/// A logger factory which is used to create loggers for system-only logs.
10+
/// </summary>
11+
internal interface ISystemLoggerFactory : ILoggerFactory
12+
{
13+
}
14+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Microsoft.Azure.WebJobs.Script.Diagnostics
8+
{
9+
/// <summary>
10+
/// Default implementation of <see cref="ISystemLoggerFactory"/>.
11+
/// </summary>
12+
/// <param name="loggerFactory">The logger factory from the root container to wrap.</param>
13+
internal class SystemLoggerFactory(ILoggerFactory loggerFactory) : ISystemLoggerFactory
14+
{
15+
public void AddProvider(ILoggerProvider provider)
16+
=> throw new InvalidOperationException("Cannot add providers to the system logger factory.");
17+
18+
public ILogger CreateLogger(string categoryName) => loggerFactory.CreateLogger(categoryName);
19+
20+
public void Dispose()
21+
{
22+
// No op - we do not dispose the provided logger factory.
23+
}
24+
}
25+
}

test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.AspNetCore.TestHost;
1717
using Microsoft.Azure.WebJobs.Host.Executors;
1818
using Microsoft.Azure.WebJobs.Script.Config;
19+
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1920
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
2021
using Microsoft.Azure.WebJobs.Script.Models;
2122
using Microsoft.Azure.WebJobs.Script.WebHost;
@@ -120,6 +121,7 @@ public TestFunctionHost(string scriptPath, string logPath, string testDataPath =
120121
return GetMetadataManager(montior, scriptManager, loggerFactory, environment);
121122
}, ServiceLifetime.Singleton));
122123

124+
services.AddSingleton<ISystemLoggerFactory, SystemLoggerFactory>();
123125
services.SkipDependencyValidation();
124126

125127
// Allows us to configure services as the last step, thereby overriding anything
@@ -352,6 +354,7 @@ public static void WriteNugetPackageSources(string appRoot, params string[] sour
352354
/// </summary>
353355
/// <returns>The messages from the WebHost LoggerProvider</returns>
354356
public IList<LogMessage> GetWebHostLogMessages() => _webHostLoggerProvider.GetAllLogMessages();
357+
public IEnumerable<LogMessage> GetWebHostLogMessages(string category) => GetWebHostLogMessages().Where(p => p.Category == category);
355358

356359
public string GetLog() => string.Join(Environment.NewLine, GetScriptHostLogMessages().Concat(GetWebHostLogMessages()).OrderBy(m => m.Timestamp));
357360

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
using System.Text;
1212
using System.Threading.Tasks;
1313
using Microsoft.Azure.WebJobs.Logging;
14-
using Microsoft.Azure.WebJobs.Script.Diagnostics;
15-
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
1614
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
1715
using Microsoft.Azure.WebJobs.Script.Workers;
1816
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,14 @@ public async Task InvokeNonAdminApi_InvalidToken_DoesNotLogTokenAuthFailure()
173173
var response = await _fixture.Host.HttpClient.SendAsync(request);
174174
response.EnsureSuccessStatusCode();
175175

176-
var validationErrors = _fixture.Host.GetScriptHostLogMessages().Where(p => p.Category == ScriptConstants.LogCategoryHostAuthentication).ToArray();
176+
var validationErrors = _fixture.Host.GetScriptHostLogMessages(ScriptConstants.LogCategoryHostAuthentication);
177177
Assert.Empty(validationErrors);
178+
179+
const string jwtCategory = "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler";
180+
var authErrors = _fixture.Host.GetScriptHostLogMessages(jwtCategory)
181+
.Concat(_fixture.Host.GetWebHostLogMessages(jwtCategory))
182+
.Where(p => p.Level == LogLevel.Error || p.Exception is not null);
183+
Assert.Empty(authErrors);
178184
}
179185

180186
public class TestFixture : EndToEndTestFixture

0 commit comments

Comments
 (0)