Skip to content

Commit 03f7777

Browse files
warwickschroederjasontaylordev
authored andcommitted
Add initial authentication supporting JWT tokens, OpenID Connect, OAuth2.0
1 parent 5f33fba commit 03f7777

File tree

8 files changed

+210
-4
lines changed

8 files changed

+210
-4
lines changed

src/Directory.Packages.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
<PackageVersion Include="Fody" Version="6.9.1" />
1818
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.1" />
1919
<PackageVersion Include="HdrHistogram" Version="2.5.0" />
20+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.22" />
21+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.22" />
2022
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.21" />
2123
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.21" />
2224
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
@@ -28,6 +30,8 @@
2830
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.1" />
2931
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
3032
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
33+
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.15.0" />
34+
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.15.0" />
3135
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
3236
<PackageVersion Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.5" />
3337
<PackageVersion Include="Mindscape.Raygun4Net.NetCore" Version="11.2.1" />

src/ServiceControl/App.config

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ These settings are only here so that we can debug ServiceControl while developin
2727
<!-- options are any comma separated combination of NLog,Seq,Otlp -->
2828
<add key="ServiceControl/LoggingProviders" value="NLog,Seq"/>
2929
<add key="ServiceControl/SeqAddress" value="http://localhost:5341"/>
30+
31+
<!-- Authentication Settings (JWT with OpenID Connect) -->
32+
<!-- Uncomment and configure to enable authentication -->
33+
<!-- Leaving 'Authentication.Enabled' commented out defaults authentication to 'false'-->
34+
<!--<add key="ServiceControl/Authentication.Enabled" value="false" />
35+
<add key="ServiceControl/Authentication.Authority" value="" />
36+
<add key="ServiceControl/Authentication.Audience" value="" />-->
37+
<!-- Optional Authentication Settings (defaults shown) -->
38+
<!--<add key="ServiceControl/Authentication.ValidateIssuer" value="true" />
39+
<add key="ServiceControl/Authentication.ValidateAudience" value="true" />
40+
<add key="ServiceControl/Authentication.ValidateLifetime" value="true" />
41+
<add key="ServiceControl/Authentication.ValidateIssuerSigningKey" value="true" />
42+
<add key="ServiceControl/Authentication.RequireHttpsMetadata" value="true" />-->
3043
</appSettings>
3144
<connectionStrings>
3245
<!-- DEVS - Pick a transport connection string to match chosen transport above -->

src/ServiceControl/Hosting/Commands/RunCommand.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
namespace ServiceControl.Hosting.Commands
22
{
3+
using System;
34
using System.Threading.Tasks;
45
using Infrastructure.WebApi;
6+
using Microsoft.AspNetCore.Authentication.JwtBearer;
57
using Microsoft.AspNetCore.Builder;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.IdentityModel.JsonWebTokens;
10+
using Microsoft.IdentityModel.Tokens;
611
using NServiceBus;
712
using Particular.ServiceControl;
813
using Particular.ServiceControl.Hosting;
@@ -20,11 +25,49 @@ public override async Task Execute(HostArguments args, Settings settings)
2025
settings.RunCleanupBundle = true;
2126

2227
var hostBuilder = WebApplication.CreateBuilder();
28+
29+
// Configure JWT Bearer Authentication with OpenID Connect
30+
if (settings.OpenIdConnectSettings.Enabled)
31+
{
32+
hostBuilder.Services.AddAuthentication(options =>
33+
{
34+
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
35+
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
36+
})
37+
.AddJwtBearer(options =>
38+
{
39+
var oidcSettings = settings.OpenIdConnectSettings;
40+
41+
options.Authority = oidcSettings.Authority;
42+
43+
// Configure token validation parameters
44+
options.TokenValidationParameters = new TokenValidationParameters
45+
{
46+
ValidateIssuer = oidcSettings.ValidateIssuer,
47+
ValidateAudience = oidcSettings.ValidateAudience,
48+
ValidateLifetime = oidcSettings.ValidateLifetime,
49+
ValidateIssuerSigningKey = oidcSettings.ValidateIssuerSigningKey,
50+
ValidAudience = oidcSettings.Audience,
51+
ClockSkew = TimeSpan.FromMinutes(5) // Allow 5 minutes clock skew
52+
};
53+
54+
options.RequireHttpsMetadata = oidcSettings.RequireHttpsMetadata;
55+
56+
// Don't map inbound claims to legacy Microsoft claim types
57+
options.MapInboundClaims = false;
58+
});
59+
60+
// Clear the default claim type mappings to use standard JWT claim names
61+
JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();
62+
}
63+
64+
2365
hostBuilder.AddServiceControl(settings, endpointConfiguration);
2466
hostBuilder.AddServiceControlApi();
2567

2668
var app = hostBuilder.Build();
27-
app.UseServiceControl();
69+
app.UseServiceControl(authenticationEnabled: settings.OpenIdConnectSettings.Enabled);
70+
2871
await app.RunAsync(settings.RootUrl);
2972
}
3073
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
namespace ServiceBus.Management.Infrastructure.Settings
2+
{
3+
using System;
4+
using System.Text.Json.Serialization;
5+
using Microsoft.Extensions.Logging;
6+
using ServiceControl.Configuration;
7+
using ServiceControl.Infrastructure;
8+
9+
public class OpenIdConnectSettings
10+
{
11+
readonly ILogger logger = LoggerUtil.CreateStaticLogger<OpenIdConnectSettings>();
12+
13+
public OpenIdConnectSettings(bool validateConfiguration)
14+
{
15+
Enabled = SettingsReader.Read(Settings.SettingsRootNamespace, "Authentication.Enabled", false);
16+
17+
if (!Enabled)
18+
{
19+
return;
20+
}
21+
22+
Authority = SettingsReader.Read<string>(Settings.SettingsRootNamespace, "Authentication.Authority");
23+
Audience = SettingsReader.Read<string>(Settings.SettingsRootNamespace, "Authentication.Audience");
24+
ValidateIssuer = SettingsReader.Read(Settings.SettingsRootNamespace, "Authentication.ValidateIssuer", true);
25+
ValidateAudience = SettingsReader.Read(Settings.SettingsRootNamespace, "Authentication.ValidateAudience", true);
26+
ValidateLifetime = SettingsReader.Read(Settings.SettingsRootNamespace, "Authentication.ValidateLifetime", true);
27+
ValidateIssuerSigningKey = SettingsReader.Read(Settings.SettingsRootNamespace, "Authentication.ValidateIssuerSigningKey", true);
28+
RequireHttpsMetadata = SettingsReader.Read(Settings.SettingsRootNamespace, "Authentication.RequireHttpsMetadata", true);
29+
30+
if (validateConfiguration)
31+
{
32+
Validate();
33+
}
34+
}
35+
36+
[JsonPropertyName("enabled")]
37+
public bool Enabled { get; }
38+
39+
[JsonPropertyName("authority")]
40+
public string Authority { get; }
41+
42+
[JsonPropertyName("audience")]
43+
public string Audience { get; }
44+
45+
[JsonPropertyName("validateIssuer")]
46+
public bool ValidateIssuer { get; }
47+
48+
[JsonPropertyName("validateAudience")]
49+
public bool ValidateAudience { get; }
50+
51+
[JsonPropertyName("validateLifetime")]
52+
public bool ValidateLifetime { get; }
53+
54+
[JsonPropertyName("validateIssuerSigningKey")]
55+
public bool ValidateIssuerSigningKey { get; }
56+
57+
[JsonPropertyName("requireHttpsMetadata")]
58+
public bool RequireHttpsMetadata { get; }
59+
60+
void Validate()
61+
{
62+
if (!Enabled)
63+
{
64+
return;
65+
}
66+
67+
if (string.IsNullOrWhiteSpace(Authority))
68+
{
69+
var message = "Authentication.Authority is required when authentication is enabled. Please provide a valid OpenID Connect authority URL (e.g., https://login.microsoftonline.com/{tenant-id}/v2.0)";
70+
logger.LogCritical(message);
71+
throw new Exception(message);
72+
}
73+
74+
if (!Uri.TryCreate(Authority, UriKind.Absolute, out var authorityUri))
75+
{
76+
var message = $"Authentication.Authority must be a valid absolute URI. Current value: '{Authority}'";
77+
logger.LogCritical(message);
78+
throw new Exception(message);
79+
}
80+
81+
if (RequireHttpsMetadata && authorityUri.Scheme != Uri.UriSchemeHttps)
82+
{
83+
var message = $"Authentication.Authority must use HTTPS when RequireHttpsMetadata is true. Current value: '{Authority}'. Either use HTTPS or set Authentication.RequireHttpsMetadata to false (not recommended for production)";
84+
logger.LogCritical(message);
85+
throw new Exception(message);
86+
}
87+
88+
if (string.IsNullOrWhiteSpace(Audience))
89+
{
90+
var message = "Authentication.Audience is required when authentication is enabled. Please provide a valid audience identifier (typically your API identifier or client ID)";
91+
logger.LogCritical(message);
92+
throw new Exception(message);
93+
}
94+
95+
if (!ValidateIssuer)
96+
{
97+
logger.LogWarning("Authentication.ValidateIssuer is set to false. This is not recommended for production environments as it may allow tokens from untrusted issuers");
98+
}
99+
100+
if (!ValidateAudience)
101+
{
102+
logger.LogWarning("Authentication.ValidateAudience is set to false. This is not recommended for production environments as it may allow tokens intended for other applications");
103+
}
104+
105+
if (!ValidateLifetime)
106+
{
107+
logger.LogWarning("Authentication.ValidateLifetime is set to false. This is not recommended as it may allow expired tokens to be accepted");
108+
}
109+
110+
if (!ValidateIssuerSigningKey)
111+
{
112+
logger.LogWarning("Authentication.ValidateIssuerSigningKey is set to false. This is a serious security risk and should only be used in development environments");
113+
}
114+
115+
logger.LogInformation("Authentication configuration validated successfully");
116+
logger.LogInformation(" Authority: {Authority}", Authority);
117+
logger.LogInformation(" Audience: {Audience}", Audience);
118+
logger.LogInformation(" ValidateIssuer: {ValidateIssuer}", ValidateIssuer);
119+
logger.LogInformation(" ValidateAudience: {ValidateAudience}", ValidateAudience);
120+
logger.LogInformation(" ValidateLifetime: {ValidateLifetime}", ValidateLifetime);
121+
logger.LogInformation(" ValidateIssuerSigningKey: {ValidateIssuerSigningKey}", ValidateIssuerSigningKey);
122+
logger.LogInformation(" RequireHttpsMetadata: {RequireHttpsMetadata}", RequireHttpsMetadata);
123+
}
124+
}
125+
}

src/ServiceControl/Infrastructure/Settings/Settings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public Settings(
3737

3838
LoadErrorIngestionSettings();
3939

40+
OpenIdConnectSettings = new OpenIdConnectSettings(ValidateConfiguration);
41+
4042
TransportConnectionString = GetConnectionString();
4143
TransportType = transportType ?? SettingsReader.Read<string>(SettingsRootNamespace, "TransportType");
4244
PersistenceType = persisterType ?? SettingsReader.Read<string>(SettingsRootNamespace, "PersistenceType");
@@ -181,6 +183,8 @@ public TimeSpan HeartbeatGracePeriod
181183

182184
public bool DisableHealthChecks { get; set; }
183185

186+
public OpenIdConnectSettings OpenIdConnectSettings { get; }
187+
184188
// The default value is set to the maximum allowed time by the most
185189
// restrictive hosting platform, which is Linux containers. Linux
186190
// containers allow for a maximum of 10 seconds. We set it to 5 to

src/ServiceControl/Infrastructure/WebApi/Cors.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public static CorsPolicy GetDefaultPolicy()
1010

1111
builder.AllowAnyOrigin();
1212
builder.WithExposedHeaders(["ETag", "Last-Modified", "Link", "Total-Count", "X-Particular-Version", "Content-Disposition"]);
13-
builder.WithHeaders(["Origin", "X-Requested-With", "Content-Type", "Accept"]);
13+
builder.WithHeaders(["Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization"]);
1414
builder.WithMethods(["POST", "GET", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"]);
1515

1616
return builder.Build();

src/ServiceControl/ServiceControl.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
</ItemGroup>
2929

3030
<ItemGroup>
31+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
32+
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
3133
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
34+
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" />
35+
<PackageReference Include="Microsoft.IdentityModel.Tokens" />
3236
<PackageReference Include="NServiceBus.CustomChecks" />
3337
<PackageReference Include="NServiceBus.Extensions.Hosting" />
3438
<PackageReference Include="NServiceBus.Extensions.Logging" />

src/ServiceControl/WebApplicationExtensions.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,27 @@ namespace ServiceControl;
77

88
public static class WebApplicationExtensions
99
{
10-
public static void UseServiceControl(this WebApplication app)
10+
public static void UseServiceControl(this WebApplication app, bool authenticationEnabled = false)
1111
{
1212
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });
1313
app.UseResponseCompression();
1414
app.UseMiddleware<BodyUrlRouteFix>();
1515
app.UseHttpLogging();
1616
app.MapHub<MessageStreamerHub>("/api/messagestream");
1717
app.UseCors();
18-
app.MapControllers();
18+
19+
// Always add middleware (harmless when not configured)
20+
app.UseAuthentication();
21+
app.UseAuthorization();
22+
23+
// Only require authorization if authentication is enabled
24+
if (authenticationEnabled)
25+
{
26+
app.MapControllers().RequireAuthorization();
27+
}
28+
else
29+
{
30+
app.MapControllers();
31+
}
1932
}
2033
}

0 commit comments

Comments
 (0)