From 1a628f990d85e0ddb3f84d2cc70c9ef8a922b445 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 7 Oct 2026 13:32:49 +0100 Subject: [PATCH 1/3] Gate legacy password grant with compatibility switch --- .../Oidc/OAuthGrantPolicy.cs | 5 +++++ .../SecurityServiceOptions.cs | 2 ++ .../Common/DockerHelper.cs | 1 + .../Oidc/OAuthGrantPolicyTests.cs | 17 +++++++++++++++++ .../HostedServices/DatabaseInitializer.cs | 10 ++++++++-- SecurityService/Program.cs | 7 +++++-- SecurityService/appsettings.json | 1 + SecurityService/appsettings.staging.json | 1 + 8 files changed, 40 insertions(+), 4 deletions(-) diff --git a/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs b/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs index fd17531..894b933 100644 --- a/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs +++ b/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs @@ -31,6 +31,11 @@ public static bool IsGrantAllowed(string grantType, string clientId, OAuthOption return true; } + if (LegacyGrantTypes.Contains(grantType) && options.EnableLegacyGrantTypes == false) + { + return false; + } + if (string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) && options.EnableHybridFlow == false) { return false; diff --git a/SecurityService.BusinessLogic/SecurityServiceOptions.cs b/SecurityService.BusinessLogic/SecurityServiceOptions.cs index ad009f3..d27314e 100644 --- a/SecurityService.BusinessLogic/SecurityServiceOptions.cs +++ b/SecurityService.BusinessLogic/SecurityServiceOptions.cs @@ -59,6 +59,8 @@ public ServiceOptions() public sealed class OAuthOptions { + public bool EnableLegacyGrantTypes { get; set; } = true; + public bool EnableHybridFlow { get; set; } public Dictionary> LegacyGrantTypeClients { get; set; } = new(StringComparer.OrdinalIgnoreCase); diff --git a/SecurityService.IntegrationTests/Common/DockerHelper.cs b/SecurityService.IntegrationTests/Common/DockerHelper.cs index e9666ea..9305e74 100644 --- a/SecurityService.IntegrationTests/Common/DockerHelper.cs +++ b/SecurityService.IntegrationTests/Common/DockerHelper.cs @@ -86,6 +86,7 @@ public override ContainerBuilder SetupSecurityServiceContainer() environmentVariables.Add("ServiceOptions:ManagementBootstrap:ClientSecret", "management-bootstrap-secret"); environmentVariables.Add("ServiceOptions:OAuth:LegacyGrantTypeClients:password:0", "merchantClient"); environmentVariables.Add("ServiceOptions:OAuth:LegacyGrantTypeClients:hybrid:0", "testclient2"); + environmentVariables.Add("ServiceOptions:OAuth:EnableLegacyGrantTypes", "true"); environmentVariables.Add("ServiceOptions:OAuth:EnableHybridFlow", "true"); environmentVariables.Add("ConnectionStrings:PersistedGrantDbContext", this.SetConnectionString($"PersistedGrantStore-{this.TestId}", this.UseSecureSqlServerDatabase)); environmentVariables.Add("ConnectionStrings:ConfigurationDbContext", this.SetConnectionString($"Configuration-{this.TestId}", this.UseSecureSqlServerDatabase)); diff --git a/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs b/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs index b57b963..4628a6d 100644 --- a/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs +++ b/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs @@ -69,6 +69,23 @@ public void HybridFlow_IsRejectedWhenFeatureIsDisabled() OAuthGrantPolicy.IsGrantAllowed("hybrid", "browser-client", options).ShouldBeFalse(); } + [Fact] + public void LegacyGrantTypes_AreRejectedWhenCompatibilitySwitchIsDisabled() + { + var options = new OAuthOptions + { + EnableLegacyGrantTypes = false, + LegacyGrantTypeClients = new(StringComparer.OrdinalIgnoreCase) + { + [GrantTypes.Password] = ["mobile-client"], + ["hybrid"] = ["browser-client"] + } + }; + + OAuthGrantPolicy.IsGrantAllowed(GrantTypes.Password, "mobile-client", options).ShouldBeFalse(); + OAuthGrantPolicy.IsGrantAllowed("hybrid", "browser-client", options).ShouldBeFalse(); + } + [Fact] public void HybridFlow_IsAllowedForConfiguredClientWhenFeatureIsEnabled() { diff --git a/SecurityService/HostedServices/DatabaseInitializer.cs b/SecurityService/HostedServices/DatabaseInitializer.cs index 6787316..c67bb49 100644 --- a/SecurityService/HostedServices/DatabaseInitializer.cs +++ b/SecurityService/HostedServices/DatabaseInitializer.cs @@ -85,10 +85,16 @@ private async Task SynchronizeApplicationPermissionsAsync(IServiceProvider servi await applicationManager.PopulateAsync(descriptor, application, cancellationToken); IReadOnlyCollection allowedGrantTypes = JsonListSerializer.Deserialize(client.AllowedGrantTypesJson); - if (this.Options.OAuth.EnableHybridFlow == false) + if (this.Options.OAuth.EnableLegacyGrantTypes == false || this.Options.OAuth.EnableHybridFlow == false) { allowedGrantTypes = allowedGrantTypes - .Where(grantType => string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) == false) + .Where(grantType => + (this.Options.OAuth.EnableLegacyGrantTypes || + (string.Equals(grantType, "password", StringComparison.OrdinalIgnoreCase) == false && + string.Equals(grantType, "implicit", StringComparison.OrdinalIgnoreCase) == false && + string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) == false)) && + (this.Options.OAuth.EnableHybridFlow || + string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) == false)) .ToArray(); } diff --git a/SecurityService/Program.cs b/SecurityService/Program.cs index 40cbcb0..a2ee156 100644 --- a/SecurityService/Program.cs +++ b/SecurityService/Program.cs @@ -304,7 +304,9 @@ .AllowClientCredentialsFlow() .AllowRefreshTokenFlow(); - if (options.OAuth.LegacyGrantTypeClients.TryGetValue(OpenIddictConstants.GrantTypes.Password, out List? passwordClients) && passwordClients.Count > 0) + if (options.OAuth.EnableLegacyGrantTypes && + options.OAuth.LegacyGrantTypeClients.TryGetValue(OpenIddictConstants.GrantTypes.Password, out List? passwordClients) && + passwordClients.Count > 0) { serverOptions.AllowPasswordFlow(); } @@ -314,7 +316,8 @@ serverOptions.AllowImplicitFlow(); } - if (options.OAuth.EnableHybridFlow && + if (options.OAuth.EnableLegacyGrantTypes && + options.OAuth.EnableHybridFlow && options.OAuth.LegacyGrantTypeClients.TryGetValue("hybrid", out List? hybridClients) && hybridClients.Count > 0) { diff --git a/SecurityService/appsettings.json b/SecurityService/appsettings.json index ea0b27a..15e2ace 100644 --- a/SecurityService/appsettings.json +++ b/SecurityService/appsettings.json @@ -31,6 +31,7 @@ "PasswordResetTokenExpiryInHours": 2 }, "OAuth": { + "EnableLegacyGrantTypes": true, "EnableHybridFlow": true, "LegacyGrantTypeClients": { "hybrid": [ diff --git a/SecurityService/appsettings.staging.json b/SecurityService/appsettings.staging.json index aca7a45..6d64708 100644 --- a/SecurityService/appsettings.staging.json +++ b/SecurityService/appsettings.staging.json @@ -3,6 +3,7 @@ "PublicOrigin": "https://192.168.1.163:5001", "IssuerUrl": "https://192.168.1.163:5001", "OAuth": { + "EnableLegacyGrantTypes": true, "EnableHybridFlow": true, "LegacyGrantTypeClients": { "hybrid": [ From 36ac4faa42605e9b26f460ab4dbac7872b6af7e7 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 7 Oct 2026 13:35:22 +0100 Subject: [PATCH 2/3] Use one legacy grant compatibility switch --- .../Oidc/OAuthGrantPolicy.cs | 5 ----- .../SecurityServiceOptions.cs | 2 -- .../Common/DockerHelper.cs | 1 - .../Oidc/OAuthGrantPolicyTests.cs | 4 ++-- SecurityService/HostedServices/DatabaseInitializer.cs | 11 ++++------- SecurityService/Program.cs | 1 - SecurityService/appsettings.json | 1 - SecurityService/appsettings.staging.json | 1 - 8 files changed, 6 insertions(+), 20 deletions(-) diff --git a/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs b/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs index 894b933..4194d2e 100644 --- a/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs +++ b/SecurityService.BusinessLogic/Oidc/OAuthGrantPolicy.cs @@ -36,11 +36,6 @@ public static bool IsGrantAllowed(string grantType, string clientId, OAuthOption return false; } - if (string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) && options.EnableHybridFlow == false) - { - return false; - } - if (LegacyGrantTypes.Contains(grantType) == false || options.LegacyGrantTypeClients.TryGetValue(grantType, out List? clients) == false) { return false; diff --git a/SecurityService.BusinessLogic/SecurityServiceOptions.cs b/SecurityService.BusinessLogic/SecurityServiceOptions.cs index d27314e..9c27309 100644 --- a/SecurityService.BusinessLogic/SecurityServiceOptions.cs +++ b/SecurityService.BusinessLogic/SecurityServiceOptions.cs @@ -61,8 +61,6 @@ public sealed class OAuthOptions { public bool EnableLegacyGrantTypes { get; set; } = true; - public bool EnableHybridFlow { get; set; } - public Dictionary> LegacyGrantTypeClients { get; set; } = new(StringComparer.OrdinalIgnoreCase); } diff --git a/SecurityService.IntegrationTests/Common/DockerHelper.cs b/SecurityService.IntegrationTests/Common/DockerHelper.cs index 9305e74..5e3c3b8 100644 --- a/SecurityService.IntegrationTests/Common/DockerHelper.cs +++ b/SecurityService.IntegrationTests/Common/DockerHelper.cs @@ -87,7 +87,6 @@ public override ContainerBuilder SetupSecurityServiceContainer() environmentVariables.Add("ServiceOptions:OAuth:LegacyGrantTypeClients:password:0", "merchantClient"); environmentVariables.Add("ServiceOptions:OAuth:LegacyGrantTypeClients:hybrid:0", "testclient2"); environmentVariables.Add("ServiceOptions:OAuth:EnableLegacyGrantTypes", "true"); - environmentVariables.Add("ServiceOptions:OAuth:EnableHybridFlow", "true"); environmentVariables.Add("ConnectionStrings:PersistedGrantDbContext", this.SetConnectionString($"PersistedGrantStore-{this.TestId}", this.UseSecureSqlServerDatabase)); environmentVariables.Add("ConnectionStrings:ConfigurationDbContext", this.SetConnectionString($"Configuration-{this.TestId}", this.UseSecureSqlServerDatabase)); environmentVariables.Add("ConnectionStrings:AuthenticationDbContext", this.SetConnectionString($"Authentication-{this.TestId}", this.UseSecureSqlServerDatabase)); diff --git a/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs b/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs index 4628a6d..591a0e3 100644 --- a/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs +++ b/SecurityService.UnitTests/Oidc/OAuthGrantPolicyTests.cs @@ -59,7 +59,7 @@ public void HybridFlow_IsRejectedWhenFeatureIsDisabled() { var options = new OAuthOptions { - EnableHybridFlow = false, + EnableLegacyGrantTypes = false, LegacyGrantTypeClients = new(StringComparer.OrdinalIgnoreCase) { ["hybrid"] = ["browser-client"] @@ -91,7 +91,7 @@ public void HybridFlow_IsAllowedForConfiguredClientWhenFeatureIsEnabled() { var options = new OAuthOptions { - EnableHybridFlow = true, + EnableLegacyGrantTypes = true, LegacyGrantTypeClients = new(StringComparer.OrdinalIgnoreCase) { ["hybrid"] = ["browser-client"] diff --git a/SecurityService/HostedServices/DatabaseInitializer.cs b/SecurityService/HostedServices/DatabaseInitializer.cs index c67bb49..85e8048 100644 --- a/SecurityService/HostedServices/DatabaseInitializer.cs +++ b/SecurityService/HostedServices/DatabaseInitializer.cs @@ -85,16 +85,13 @@ private async Task SynchronizeApplicationPermissionsAsync(IServiceProvider servi await applicationManager.PopulateAsync(descriptor, application, cancellationToken); IReadOnlyCollection allowedGrantTypes = JsonListSerializer.Deserialize(client.AllowedGrantTypesJson); - if (this.Options.OAuth.EnableLegacyGrantTypes == false || this.Options.OAuth.EnableHybridFlow == false) + if (this.Options.OAuth.EnableLegacyGrantTypes == false) { allowedGrantTypes = allowedGrantTypes .Where(grantType => - (this.Options.OAuth.EnableLegacyGrantTypes || - (string.Equals(grantType, "password", StringComparison.OrdinalIgnoreCase) == false && - string.Equals(grantType, "implicit", StringComparison.OrdinalIgnoreCase) == false && - string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) == false)) && - (this.Options.OAuth.EnableHybridFlow || - string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) == false)) + string.Equals(grantType, "password", StringComparison.OrdinalIgnoreCase) == false && + string.Equals(grantType, "implicit", StringComparison.OrdinalIgnoreCase) == false && + string.Equals(grantType, "hybrid", StringComparison.OrdinalIgnoreCase) == false) .ToArray(); } diff --git a/SecurityService/Program.cs b/SecurityService/Program.cs index a2ee156..e2c73ed 100644 --- a/SecurityService/Program.cs +++ b/SecurityService/Program.cs @@ -317,7 +317,6 @@ } if (options.OAuth.EnableLegacyGrantTypes && - options.OAuth.EnableHybridFlow && options.OAuth.LegacyGrantTypeClients.TryGetValue("hybrid", out List? hybridClients) && hybridClients.Count > 0) { diff --git a/SecurityService/appsettings.json b/SecurityService/appsettings.json index 15e2ace..af45c13 100644 --- a/SecurityService/appsettings.json +++ b/SecurityService/appsettings.json @@ -32,7 +32,6 @@ }, "OAuth": { "EnableLegacyGrantTypes": true, - "EnableHybridFlow": true, "LegacyGrantTypeClients": { "hybrid": [ "managementUIClient" diff --git a/SecurityService/appsettings.staging.json b/SecurityService/appsettings.staging.json index 6d64708..2d7d1fa 100644 --- a/SecurityService/appsettings.staging.json +++ b/SecurityService/appsettings.staging.json @@ -4,7 +4,6 @@ "IssuerUrl": "https://192.168.1.163:5001", "OAuth": { "EnableLegacyGrantTypes": true, - "EnableHybridFlow": true, "LegacyGrantTypeClients": { "hybrid": [ "managementUIClient" From f85f9457d92bfcad62564f6335935fae064c6c99 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 7 Oct 2026 13:56:05 +0100 Subject: [PATCH 3/3] Gate implicit flow with legacy grant switch --- SecurityService/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SecurityService/Program.cs b/SecurityService/Program.cs index e2c73ed..56d59d6 100644 --- a/SecurityService/Program.cs +++ b/SecurityService/Program.cs @@ -311,7 +311,9 @@ serverOptions.AllowPasswordFlow(); } - if (options.OAuth.LegacyGrantTypeClients.TryGetValue(OpenIddictConstants.GrantTypes.Implicit, out List? implicitClients) && implicitClients.Count > 0) + if (options.OAuth.EnableLegacyGrantTypes && + options.OAuth.LegacyGrantTypeClients.TryGetValue(OpenIddictConstants.GrantTypes.Implicit, out List? implicitClients) && + implicitClients.Count > 0) { serverOptions.AllowImplicitFlow(); }