Skip to content

Commit 1b3f5dc

Browse files
Add auth to other instances
1 parent 352b4c8 commit 1b3f5dc

File tree

14 files changed

+287
-220
lines changed

14 files changed

+287
-220
lines changed

src/ServiceControl.Audit/App.config

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

src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks;
44
using Microsoft.AspNetCore.Builder;
55
using NServiceBus;
6+
using ServiceControl.Hosting.Auth;
67
using Settings;
78
using WebApi;
89

@@ -15,6 +16,8 @@ public override async Task Execute(HostArguments args, Settings settings)
1516
assemblyScanner.ExcludeAssemblies("ServiceControl.Plugin");
1617

1718
var hostBuilder = WebApplication.CreateBuilder();
19+
20+
hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings);
1821
hostBuilder.AddServiceControlAudit((_, __) =>
1922
{
2023
//Do nothing. The transports in NSB 8 are designed to handle broker outages. Audit ingestion will be paused when broker is unavailable.
@@ -24,6 +27,8 @@ public override async Task Execute(HostArguments args, Settings settings)
2427

2528
var app = hostBuilder.Build();
2629
app.UseServiceControlAudit();
30+
app.UseServiceControlAuthentication(authenticationEnabled: settings.OpenIdConnectSettings.Enabled);
31+
2732
await app.RunAsync(settings.RootUrl);
2833
}
2934
}

src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public Settings(string transportType = null, string persisterType = null, Loggin
1717
{
1818
LoggingSettings = loggingSettings ?? new(SettingsRootNamespace);
1919

20+
OpenIdConnectSettings = new OpenIdConnectSettings(SettingsRootNamespace, ValidateConfiguration);
21+
2022
// Overwrite the instance name if it is specified in ENVVAR, reg, or config file -- LEGACY SETTING NAME
2123
InstanceName = SettingsReader.Read(SettingsRootNamespace, "InternalQueueName", InstanceName);
2224

@@ -92,6 +94,8 @@ void LoadAuditQueueInformation()
9294

9395
public LoggingSettings LoggingSettings { get; }
9496

97+
public OpenIdConnectSettings OpenIdConnectSettings { get; }
98+
9599
//HINT: acceptance tests only
96100
public Func<MessageContext, bool> MessageFilter { get; set; }
97101

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace ServiceControl.Hosting.Auth
2+
{
3+
using System;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Hosting;
6+
using ServiceControl.Infrastructure;
7+
8+
public static class HostApplicationBuilderExtensions
9+
{
10+
public static void AddServiceControlAuthentication(this IHostApplicationBuilder hostBuilder, OpenIdConnectSettings oidcSettings)
11+
{
12+
if (!oidcSettings.Enabled)
13+
{
14+
return;
15+
}
16+
17+
hostBuilder.Services.AddAuthentication(options =>
18+
{
19+
options.DefaultScheme = "Bearer";
20+
options.DefaultChallengeScheme = "Bearer";
21+
})
22+
.AddJwtBearer("Bearer", options =>
23+
{
24+
options.Authority = oidcSettings.Authority;
25+
// Configure token validation parameters
26+
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
27+
{
28+
ValidateIssuer = oidcSettings.ValidateIssuer,
29+
ValidateAudience = oidcSettings.ValidateAudience,
30+
ValidateLifetime = oidcSettings.ValidateLifetime,
31+
ValidateIssuerSigningKey = oidcSettings.ValidateIssuerSigningKey,
32+
ValidAudience = oidcSettings.Audience,
33+
ClockSkew = TimeSpan.FromMinutes(5) // Allow 5 minutes clock skew
34+
};
35+
options.RequireHttpsMetadata = oidcSettings.RequireHttpsMetadata;
36+
// Don't map inbound claims to legacy Microsoft claim types
37+
options.MapInboundClaims = false;
38+
});
39+
}
40+
}
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace ServiceControl.Hosting.Auth
2+
{
3+
using Microsoft.AspNetCore.Builder;
4+
5+
public static class WebApplicationExtensions
6+
{
7+
public static void UseServiceControlAuthentication(this WebApplication app, bool authenticationEnabled = false)
8+
{
9+
if (authenticationEnabled)
10+
{
11+
app.UseAuthentication();
12+
app.UseAuthorization();
13+
app.MapControllers().RequireAuthorization();
14+
}
15+
else
16+
{
17+
app.MapControllers();
18+
}
19+
}
20+
}
21+
}

src/ServiceControl.Hosting/ServiceControl.Hosting.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
9+
</ItemGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
13+
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
814
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
915
<PackageReference Include="NLog.Extensions.Logging" />
1016
</ItemGroup>
1117

18+
<ItemGroup>
19+
<ProjectReference Include="..\ServiceControl.Infrastructure\ServiceControl.Infrastructure.csproj" />
20+
</ItemGroup>
21+
1222
</Project>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
namespace ServiceControl.Infrastructure;
2+
3+
using System;
4+
using System.Text.Json.Serialization;
5+
using Microsoft.Extensions.Logging;
6+
using ServiceControl.Configuration;
7+
8+
public class OpenIdConnectSettings
9+
{
10+
readonly ILogger logger = LoggerUtil.CreateStaticLogger<OpenIdConnectSettings>();
11+
12+
public OpenIdConnectSettings(SettingsRootNamespace rootNamespace, bool validateConfiguration)
13+
{
14+
Enabled = SettingsReader.Read(rootNamespace, "Authentication.Enabled", false);
15+
16+
if (!Enabled)
17+
{
18+
return;
19+
}
20+
21+
Authority = SettingsReader.Read<string>(rootNamespace, "Authentication.Authority");
22+
Audience = SettingsReader.Read<string>(rootNamespace, "Authentication.Audience");
23+
ValidateIssuer = SettingsReader.Read(rootNamespace, "Authentication.ValidateIssuer", true);
24+
ValidateAudience = SettingsReader.Read(rootNamespace, "Authentication.ValidateAudience", true);
25+
ValidateLifetime = SettingsReader.Read(rootNamespace, "Authentication.ValidateLifetime", true);
26+
ValidateIssuerSigningKey = SettingsReader.Read(rootNamespace, "Authentication.ValidateIssuerSigningKey", true);
27+
RequireHttpsMetadata = SettingsReader.Read(rootNamespace, "Authentication.RequireHttpsMetadata", true);
28+
ServicePulseClientId = SettingsReader.Read<string>(rootNamespace, "Authentication.ServicePulse.ClientId");
29+
ServicePulseApiScope = SettingsReader.Read<string>(rootNamespace, "Authentication.ServicePulse.ApiScope");
30+
ServicePulseAuthority = SettingsReader.Read<string>(rootNamespace, "Authentication.ServicePulse.Authority");
31+
32+
if (validateConfiguration)
33+
{
34+
Validate();
35+
}
36+
}
37+
38+
[JsonPropertyName("enabled")]
39+
public bool Enabled { get; }
40+
41+
[JsonPropertyName("authority")]
42+
public string Authority { get; }
43+
44+
[JsonPropertyName("audience")]
45+
public string Audience { get; }
46+
47+
[JsonPropertyName("validateIssuer")]
48+
public bool ValidateIssuer { get; }
49+
50+
[JsonPropertyName("validateAudience")]
51+
public bool ValidateAudience { get; }
52+
53+
[JsonPropertyName("validateLifetime")]
54+
public bool ValidateLifetime { get; }
55+
56+
[JsonPropertyName("validateIssuerSigningKey")]
57+
public bool ValidateIssuerSigningKey { get; }
58+
59+
[JsonPropertyName("requireHttpsMetadata")]
60+
public bool RequireHttpsMetadata { get; }
61+
62+
[JsonPropertyName("servicePulseAuthority")]
63+
public string ServicePulseAuthority { get; }
64+
65+
[JsonPropertyName("servicePulseClientId")]
66+
public string ServicePulseClientId { get; }
67+
68+
[JsonPropertyName("servicePulseApiScope")]
69+
public string ServicePulseApiScope { get; }
70+
71+
void Validate()
72+
{
73+
if (!Enabled)
74+
{
75+
return;
76+
}
77+
78+
if (string.IsNullOrWhiteSpace(Authority))
79+
{
80+
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)";
81+
logger.LogCritical(message);
82+
throw new Exception(message);
83+
}
84+
85+
if (!Uri.TryCreate(Authority, UriKind.Absolute, out var authorityUri))
86+
{
87+
var message = $"Authentication.Authority must be a valid absolute URI. Current value: '{Authority}'";
88+
logger.LogCritical(message);
89+
throw new Exception(message);
90+
}
91+
92+
if (RequireHttpsMetadata && authorityUri.Scheme != Uri.UriSchemeHttps)
93+
{
94+
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)";
95+
logger.LogCritical(message);
96+
throw new Exception(message);
97+
}
98+
99+
if (string.IsNullOrWhiteSpace(Audience))
100+
{
101+
var message = "Authentication.Audience is required when authentication is enabled. Please provide a valid audience identifier (typically your API identifier or client ID)";
102+
logger.LogCritical(message);
103+
throw new Exception(message);
104+
}
105+
106+
if (!ValidateIssuer)
107+
{
108+
logger.LogWarning("Authentication.ValidateIssuer is set to false. This is not recommended for production environments as it may allow tokens from untrusted issuers");
109+
}
110+
111+
if (!ValidateAudience)
112+
{
113+
logger.LogWarning("Authentication.ValidateAudience is set to false. This is not recommended for production environments as it may allow tokens intended for other applications");
114+
}
115+
116+
if (!ValidateLifetime)
117+
{
118+
logger.LogWarning("Authentication.ValidateLifetime is set to false. This is not recommended as it may allow expired tokens to be accepted");
119+
}
120+
121+
if (!ValidateIssuerSigningKey)
122+
{
123+
logger.LogWarning("Authentication.ValidateIssuerSigningKey is set to false. This is a serious security risk and should only be used in development environments");
124+
}
125+
126+
if (string.IsNullOrWhiteSpace(ServicePulseClientId))
127+
{
128+
throw new Exception("Authentication.ServicePulse.ClientId is required when Authentication.ServicePulse.Enabled is true.");
129+
}
130+
131+
if (string.IsNullOrWhiteSpace(ServicePulseApiScope))
132+
{
133+
throw new Exception("Authentication.ServicePulse.ApiScope is required when Authentication.ServicePulse.Enabled is true.");
134+
}
135+
136+
if (ServicePulseAuthority != null && !Uri.TryCreate(ServicePulseAuthority, UriKind.Absolute, out _))
137+
{
138+
throw new Exception("Authentication.ServicePulse.Authority must be a valid absolute URI if provided.");
139+
}
140+
141+
logger.LogInformation("Authentication configuration validated successfully");
142+
logger.LogInformation(" Authority: {Authority}", Authority);
143+
logger.LogInformation(" Audience: {Audience}", Audience);
144+
logger.LogInformation(" ValidateIssuer: {ValidateIssuer}", ValidateIssuer);
145+
logger.LogInformation(" ValidateAudience: {ValidateAudience}", ValidateAudience);
146+
logger.LogInformation(" ValidateLifetime: {ValidateLifetime}", ValidateLifetime);
147+
logger.LogInformation(" ValidateIssuerSigningKey: {ValidateIssuerSigningKey}", ValidateIssuerSigningKey);
148+
logger.LogInformation(" RequireHttpsMetadata: {RequireHttpsMetadata}", RequireHttpsMetadata);
149+
logger.LogInformation(" ServicePulseClientId: {ServicePulseClientId}", ServicePulseClientId);
150+
logger.LogInformation(" ServicePulseAuthority: {ServicePulseAuthority}", ServicePulseAuthority);
151+
logger.LogInformation(" ServicePulseApiScope: {ServicePulseApiScope}", ServicePulseApiScope);
152+
}
153+
}

src/ServiceControl.Monitoring/App.config

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

src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace ServiceControl.Monitoring
55
using Infrastructure.WebApi;
66
using Microsoft.AspNetCore.Builder;
77
using NServiceBus;
8+
using ServiceControl.Hosting.Auth;
89

910
class RunCommand : AbstractCommand
1011
{
@@ -13,11 +14,14 @@ public override async Task Execute(HostArguments args, Settings settings)
1314
var endpointConfiguration = new EndpointConfiguration(settings.InstanceName);
1415

1516
var hostBuilder = WebApplication.CreateBuilder();
17+
hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings);
1618
hostBuilder.AddServiceControlMonitoring((_, __) => Task.CompletedTask, settings, endpointConfiguration);
1719
hostBuilder.AddServiceControlMonitoringApi();
1820

1921
var app = hostBuilder.Build();
2022
app.UseServiceControlMonitoring();
23+
app.UseServiceControlAuthentication(authenticationEnabled: settings.OpenIdConnectSettings.Enabled);
24+
2125
await app.RunAsync(settings.RootUrl);
2226
}
2327
}

src/ServiceControl.Monitoring/Settings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public Settings(LoggingSettings loggingSettings = null, string transportType = n
1616
{
1717
LoggingSettings = loggingSettings ?? new(SettingsRootNamespace);
1818

19+
OpenIdConnectSettings = new OpenIdConnectSettings(SettingsRootNamespace, false);
20+
1921
// Overwrite the instance name if it is specified in ENVVAR, reg, or config file
2022
InstanceName = SettingsReader.Read(SettingsRootNamespace, "InstanceName", InstanceName);
2123

@@ -49,6 +51,8 @@ public Settings(LoggingSettings loggingSettings = null, string transportType = n
4951

5052
public LoggingSettings LoggingSettings { get; }
5153

54+
public OpenIdConnectSettings OpenIdConnectSettings { get; }
55+
5256
public string InstanceName { get; init; } = DEFAULT_INSTANCE_NAME;
5357

5458
public string TransportType { get; set; }

0 commit comments

Comments
 (0)