Repository navigation
Restrict OAuth grant flows by client capability #1743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
fb3b41f
Document OAuth grant restriction design
StuartFerguson 8ad6c01
Restrict OAuth grant flows by client capability
StuartFerguson f9926a6
Add OAuth UI failure diagnostics
StuartFerguson 379fe4e
Enable legacy hybrid protocol capability
StuartFerguson 875450a
Migrate OIDC UI tests to authorization code flow
StuartFerguson dad3927
Capture current UI login failure details
StuartFerguson ce578ca
Allow offline access for OIDC UI client
StuartFerguson c186b7e
Capture current OAuth UI failure details
StuartFerguson 3737469
Allow offline access for OAuth clients
StuartFerguson 70132ed
Grant refresh tokens for offline access clients
StuartFerguson 452c9f4
Correct offline access permission assertion
StuartFerguson 5e1a6c1
Preserve bootstrap client grant restrictions
StuartFerguson 10b3da3
Avoid logger dependency in bootstrap test
StuartFerguson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
.superpowers/sdd/2026-10-06-oauth-grant-restrictions/progress.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # SDD ledger — plan: docs/superpowers/plans/2026-10-06-oauth-grant-restrictions.md | ||
|
|
||
| Pre-flight: Task 1 produces `OAuthGrantPolicy` and `OAuthOptions`, consumed by Tasks 2–4. Task 2 produces client descriptor permissions consumed by Task 4 synchronization. Task 3 produces server capability configuration consumed by Tasks 2 and 4. No other shared-interface conflicts found. | ||
|
|
||
| Ruling: Work in the current checkout — the user selected native execution, the repository is already the requested project, and creating a separate worktree is unavailable in this managed workspace; cost if wrong: changes are made on the current branch and may need manual branch isolation later. | ||
|
|
||
| Task 1: complete. Added OAuth grant policy configuration/helper and four focused tests. Verification: `dotnet build SecurityService.UnitTests/SecurityService.UnitTests.csproj --no-restore` passed; the Microsoft Testing Platform executable ran the four focused tests with 4 passed, 0 failed. Ruling: `OAuthOptions` was introduced alongside the policy helper because the policy tests consume the configuration contract; cost if wrong: a small early configuration surface that can be adjusted in the follow-up wiring task. | ||
| Task 2: complete. Client registration now validates configured grants through `OAuthGrantPolicy` and persists the derived OpenIddict permissions/requirements, including PKCE for authorization-code clients. Verification: unit-test project build passed; `ClientRequestHandlerTests` passed (6/6). Commit attempt deferred because Git index locking is currently denied in this checkout. | ||
| Task 3: complete. Server defaults now enable only authorization code, client credentials, and refresh tokens; legacy capabilities are enabled only for non-empty configured client lists; device endpoints and grant-permission bypass were removed. Shipped configuration examples and deployment documentation now show the empty legacy map. | ||
| Task 4: complete. Startup reconciliation updates existing OpenIddict applications from persisted grant lists and fails on unconfigured legacy grants; management bootstrap remains client-credentials-only and is synchronized as well. Verification: management bootstrap tests passed (3/3), application builds passed. | ||
| Task 5/6: complete for this scope. Removed device-code handler routing, preserved explicit legacy integration fixtures via test-only environment configuration, and added OAuth rollout documentation. Focused policy tests passed (4/4); client registration tests passed (6/6); integration projects build successfully. | ||
| Final verification: full unit suite built and ran 120 tests; 118 passed and 2 certificate-loader tests failed because the test PFX files were unavailable in the environment. `git diff --check` passed. Git commit remains unavailable because this checkout denies `.git/index.lock` creation. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| using System.Collections.Immutable; | ||
| using OpenIddict.Abstractions; | ||
| using static OpenIddict.Abstractions.OpenIddictConstants; | ||
|
|
||
| namespace SecurityService.BusinessLogic.Oidc; | ||
|
|
||
| public sealed record OAuthGrantPolicyResult( | ||
| ImmutableHashSet<string> Permissions, | ||
| ImmutableHashSet<string> Requirements); | ||
|
|
||
| public static class OAuthGrantPolicy | ||
| { | ||
| private static readonly HashSet<string> ApprovedGrantTypes = new(StringComparer.OrdinalIgnoreCase) | ||
| { | ||
| GrantTypes.AuthorizationCode, | ||
| GrantTypes.ClientCredentials, | ||
| GrantTypes.RefreshToken | ||
| }; | ||
|
|
||
| private static readonly HashSet<string> LegacyGrantTypes = new(StringComparer.OrdinalIgnoreCase) | ||
| { | ||
| GrantTypes.Password, | ||
| GrantTypes.Implicit, | ||
| "hybrid" | ||
| }; | ||
|
|
||
| public static bool IsGrantAllowed(string grantType, string clientId, OAuthOptions options) | ||
| { | ||
| if (ApprovedGrantTypes.Contains(grantType)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| if (LegacyGrantTypes.Contains(grantType) == false || options.LegacyGrantTypeClients.TryGetValue(grantType, out List<string>? clients) == false) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| return clients.Any(client => string.Equals(client, clientId, StringComparison.OrdinalIgnoreCase)); | ||
| } | ||
|
|
||
| public static OAuthGrantPolicyResult CreatePermissions( | ||
| IReadOnlyCollection<string> allowedGrantTypes, | ||
| string clientId, | ||
| OAuthOptions options) | ||
| { | ||
| ImmutableHashSet<string>.Builder permissions = ImmutableHashSet.CreateBuilder<string>(StringComparer.Ordinal); | ||
| ImmutableHashSet<string>.Builder requirements = ImmutableHashSet.CreateBuilder<string>(StringComparer.Ordinal); | ||
|
|
||
| foreach (string grantType in allowedGrantTypes.Distinct(StringComparer.OrdinalIgnoreCase)) | ||
| { | ||
| if (IsGrantAllowed(grantType, clientId, options) == false) | ||
| { | ||
| throw new InvalidOperationException($"Grant type '{grantType}' is not allowed for client '{clientId}'."); | ||
| } | ||
|
|
||
| switch (grantType.ToLowerInvariant()) | ||
| { | ||
| case GrantTypes.AuthorizationCode: | ||
| permissions.Add(Permissions.Endpoints.Authorization); | ||
| permissions.Add(Permissions.Endpoints.Token); | ||
| permissions.Add(Permissions.GrantTypes.AuthorizationCode); | ||
| permissions.Add(Permissions.ResponseTypes.Code); | ||
| requirements.Add(Requirements.Features.ProofKeyForCodeExchange); | ||
| break; | ||
|
|
||
| case GrantTypes.ClientCredentials: | ||
| permissions.Add(Permissions.Endpoints.Token); | ||
| permissions.Add(Permissions.GrantTypes.ClientCredentials); | ||
| break; | ||
|
|
||
| case GrantTypes.RefreshToken: | ||
| permissions.Add(Permissions.Endpoints.Token); | ||
| permissions.Add(Permissions.GrantTypes.RefreshToken); | ||
| break; | ||
|
|
||
| case GrantTypes.Password: | ||
| permissions.Add(Permissions.Endpoints.Token); | ||
| permissions.Add(Permissions.GrantTypes.Password); | ||
| break; | ||
|
|
||
| case GrantTypes.Implicit: | ||
| permissions.Add(Permissions.Endpoints.Authorization); | ||
| permissions.Add(Permissions.GrantTypes.Implicit); | ||
| permissions.Add(Permissions.ResponseTypes.Token); | ||
| permissions.Add(Permissions.ResponseTypes.IdToken); | ||
| break; | ||
|
|
||
| case "hybrid": | ||
| permissions.Add(Permissions.Endpoints.Authorization); | ||
| permissions.Add(Permissions.Endpoints.Token); | ||
| permissions.Add(Permissions.GrantTypes.AuthorizationCode); | ||
| permissions.Add(Permissions.ResponseTypes.CodeIdToken); | ||
| break; | ||
|
|
||
| default: | ||
| throw new InvalidOperationException($"Unsupported grant type '{grantType}'."); | ||
| } | ||
| } | ||
|
|
||
| return new OAuthGrantPolicyResult(permissions.ToImmutable(), requirements.ToImmutable()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 2 additions & 2 deletions
4
SecurityService.OpenIdConnect.IntegrationTests/ChangePassword/ChangePassword.feature.cs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 2 additions & 2 deletions
4
SecurityService.OpenIdConnect.IntegrationTests/ForgotPassword/ForgotPassword.feature.cs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 2 additions & 2 deletions
4
SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLogin.feature.cs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.