feat: configurable scopes, config bounds, and two clearer failures - #78
Merged
Conversation
Scopes are a per-provider option, defaulting to the openid/email/profile the bundle has always asked for. A list or a space-separated string, so the value can come from an environment variable. `openid` is required: OIDC Core §3.1.2.1 defines an authentication request as one that asks for it, and without it the provider returns no ID token. `leeway` and `cache_duration` reject a negative value at compile time. A negative leeway used to fail at the first login that needed it; a negative cache duration passed through unnoticed. The authenticator implements InteractiveAuthenticatorInterface, so a completed login dispatches security.interactive_login and remember-me treats the token as one a user asked for. A firewall declared `stateless: true` now throws StatelessFirewallException naming the setting, rather than surfacing Symfony's SessionNotFoundException as an unexplained 500.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #78 +/- ##
=============================================
- Coverage 99.74% 99.52% -0.23%
- Complexity 201 205 +4
=============================================
Files 15 15
Lines 799 834 +35
=============================================
+ Hits 797 830 +33
- Misses 2 4 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Four small items that had been queued behind the callback and PKCE work. Nothing here changes what a correctly configured application does today.
Changes
scopesdefaults toopenid,email,profile— what the bundle has always asked for. A list or a space-separated string, so the value can come from an environment variable.leewayandcache_durationreject a negative value at compile time, and accept zero.OpenIdLoginAuthenticatorimplementsInteractiveAuthenticatorInterface.StatelessFirewallException.OpenIdConfigurationProviderManager::getScopes().Why
Scopes were the literal
'openid email profile'atLoginController.php:83. An application needinggroups, an API audience, or a provider-specific scope had no way to ask for one short of overriding the controller.openidis required and enforced at compile time. OpenID Connect Core 1.0 §3.1.2.1 defines an authentication request as one that asks for it; without it the provider answers with a plain OAuth2 grant and no ID token, and an ID token is the only thing this bundle knows how to validate. Failing at container compile beats failing at the first login.The bounds close two different holes. A negative
leewayreached the library and came back asNegativeLeewayExceptionat the first login that needed it. A negativecache_durationwent into the cache unnoticed. Both are now a compile error, and both still accept0— no clock-skew window and no caching are each coherent settings.isInteractive()makes Symfony dispatchsecurity.interactive_loginand lets remember-me treat the resulting token as one a user actually asked for. Both are true of every login this authenticator completes. Session-fixation protection is unaffected either way.The stateless firewall case: the authorization code flow spans two requests, and the state, nonce and PKCE verifier wait in the session between them. On a firewall declared
stateless: truethere is nowhere to put them, so$request->getSession()threw Symfony'sSessionNotFoundExceptionand the application got an unexplained 500. It now throwsStatelessFirewallException— a\LogicException, since the fix is insecurity.yamlrather than in a retry — naming the setting to remove and chaining the original.Decisions worth reviewing
scopesnormalises a string rather than taking only a list.%env()%can carry a scalar and nothing else, and the space-delimited form is what RFC 6749 §3.3 already puts on the wire. Splitting on whitespace runs withPREG_SPLIT_NO_EMPTYhandles the leading and trailing spaces an env var picks up.getScopes()'s fallback — with a comment on each saying so. The fallback is unreachable through the container; it is there so a manager built by hand in a test cannot silently ask for no scopes.StatelessFirewallExceptionis a new concrete type, the bundle's first\LogicException. A consumer will not catch it — the point is the message — but the bundle's contract says everything thrown from a public method implements the marker interface, so it needs a type of its own rather than a bare SPL exception.min(0), notmin(1), oncache_duration. Zero means fetch the discovery document every time. Wasteful, but a coherent thing to ask for, and not the bundle's business to forbid.Tests
364 tests, 957 assertions.
openidas both a list and a string; rejected empty; negativeleewayandcache_durationrejected; zero accepted for both.isInteractive(); a request with no session throwsStatelessFirewallExceptionnamingstateless: trueand chainingSessionNotFoundException.ExceptionHierarchyTestgains the new concrete, pinning it to\LogicExceptionand the bundle marker.The escaped mutant is the pre-existing one in
ItkDevOpenIdConnectExtension.php:114, untouched here. Same local matrix caveat as the previous two PRs: composer's parallel extraction races in the matrix containers, so I ran each combination with install and phpunit in a single invocation.This completes the hardening set — B1 through B7 are now all either merged or in review.