feat(oidc)!: phase 1 spec conformance fixes (with /userinfo breaking change)#591
Merged
lakhansamani merged 11 commits intomainfrom Apr 7, 2026
Merged
feat(oidc)!: phase 1 spec conformance fixes (with /userinfo breaking change)#591lakhansamani merged 11 commits intomainfrom
lakhansamani merged 11 commits intomainfrom
Conversation
- at_hash was being set to the nonce value in the implicit/token branch instead of cfg.AtHash (the correct sha256 left-half of the access token). - nonce was never echoed in the auth_code branch even when present in the auth request, violating OIDC Core §2. - Replace the if/else with three independent claim assignments driven by cfg.AtHash, cfg.CodeHash, and cfg.Nonce, matching OIDC Core §3.1.3.6 / §3.2.2.10. c_hash branch is reserved for Phase 3 hybrid flows. - Add TestIDTokenClaimsCompliance covering all three properties. - Expose TokenProvider on the integration test setup so tests can exercise CreateAuthToken / ParseJWTToken directly.
The previous comment encoded the misconception that drove the at_hash bug (treating nonce as flow-dependent and mutually exclusive with at_hash). Replace with a one-line pointer to the in-function rules block.
Backward-compatible bool flag (default false) that will be wired to the /userinfo handler in the next commit to enable OIDC Core §5.4 scope-based claim filtering. Default false preserves existing lenient behavior so the upgrade does not break clients that request only the openid scope but consume profile/email claims.
…ency Codebase consistently uses 'UserInfo' (camel case, two words) elsewhere (AppleUserInfo, processGoogleUserInfo, etc). Align the new field to that convention before Task 3 references it. Also tighten the --help string and drop the non-ASCII section sign for terminal portability.
Implements OIDC Core §5.4 scope-based claim filtering on the /userinfo endpoint. Wired to OIDCStrictUserInfoScopes from the previous commit: - false (default): existing lenient behavior preserved — full user object returned regardless of scopes. sub is always present. - true: only sub plus the standard claim groups for the scopes encoded in the access token (profile, email, phone, address) are returned. Keys in a granted scope group are always emitted (with JSON null when the user has no value) so callers see a stable schema. Adds extractScopesFromAccessToken (handles string and array forms of the scope claim) and filterUserInfoByScopes. New integration tests cover the lenient default and five strict-mode scope combinations.
…ranch
Code review feedback on Task 3:
- Convert userInfoProfileClaims / Email / Phone / Address from
map[string]struct{} to []string. The only operation is iteration; the
map-set shape advertised a membership test that was never used and
added reader cognitive load. Slices are also iteration-safe against
future accidental delete/assign mutations of a shared package var.
- Remove the dead 'case []string:' branch in extractScopesFromAccessToken.
golang-jwt v4 MapClaims always decodes JSON arrays as []interface{};
no path in this codebase ever lands a []string in claims['scope'].
- Tighten doc comments on both the claim-group vars (MUST NOT be
mutated) and extractScopesFromAccessToken (cite RFC 6749 §3.3 and
clarify which form produces which Go type).
- Reference OIDC Core §5.3.2 explicitly in the null-emission comment.
…tion_endpoint - grant_types_supported now includes 'implicit'. /authorize already accepts response_type=token and response_type=id_token, so the prior list (authorization_code, refresh_token) was misleading. - registration_endpoint was pointing at /app (the signup UI) which is not an RFC 7591 dynamic client registration endpoint. Spec-compliant OIDC clients would fail trying to use it. Removed until RFC 7591 is actually implemented (tracked in Phase 4 roadmap).
Code review feedback on Task 4: - Add require.Equal(200) + require.NoError(json.Unmarshal) to both new TestOpenIDDiscoveryCompliance sub-tests. The registration_endpoint_absent test in particular had a latent false-positive mode: a non-JSON response would decode to an empty map and trivially pass the key-absence check. Guarding the status and unmarshal error closes that gap. - Add an inline comment in openid_config.go pointing at the invariant between response_types_supported and grant_types_supported (implicit grant ↔ response_type=token/id_token).
CHANGELOG entries for the 5 Phase 1 items (at_hash fix, nonce echo, registration_endpoint removal, implicit grant advertised, new --oidc-strict-userinfo-scopes flag). docs/oauth2-oidc-endpoints.md gains a subsection explaining the new strict userinfo scope filtering mode, including the scope → claim mapping table and the null-for-missing-key schema stability note.
…n flag BREAKING CHANGE: /userinfo now always filters returned claims by the scope set encoded in the access token, per OIDC Core §5.4. There is no longer a lenient mode or opt-in flag. Previously this branch introduced a --oidc-strict-userinfo-scopes flag (default false) to preserve backward compatibility for clients that requested only 'openid' but read profile/email claims from /userinfo. That flag is now removed: spec correctness wins. Clients must request the scopes they actually consume. Impact: - Clients requesting 'openid' only now receive only 'sub' from /userinfo. - Clients requesting 'openid email' receive sub + email + email_verified. - Clients requesting 'openid profile' receive sub + all profile claim keys (values may be JSON null when the user has no value for a claim; keys are present so response schema is stable). - 'phone' and 'address' scope groups are defined but the underlying User schema currently has no address claim, so address is effectively a no-op until the schema gains an address field. Changes: - Remove Config.OIDCStrictUserInfoScopes field. - Remove --oidc-strict-userinfo-scopes CLI flag binding. - Remove the conditional branch in UserInfoHandler — scope filtering is now the only code path. - Collapse TestUserInfoScopeFilteringLenient + TestUserInfoScopeFilteringStrict into a single TestUserInfoScopeFiltering with the 5 strict sub-tests. - Update CHANGELOG to mark this as a BREAKING change in the Changed section, removing the prior Added entry for the flag. - Update docs/oauth2-oidc-endpoints.md /userinfo section: remove the opt-in flag discussion, document the scope→claim mapping as the default unconditional behavior, add three example responses (openid, openid email, openid profile email).
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.
Summary
OIDC Phase 1 spec conformance — fixes driven by an audit against the OpenID Connect Handbook and the underlying OIDC Core 1.0 / OAuth 2.0 RFCs.
Each task has been individually spec-reviewed + code-quality-reviewed, plus a final end-of-phase holistic review (all green, zero critical/important issues).
/userinfonow strictly filters claims by scope per OIDC Core §5.4. The endpoint returns onlysubplus the claims permitted by the standard scope groups (profile,email,phone,address) encoded on the access token.Previously,
/userinforeturned the full user object regardless of scopes. Clients that request only theopenidscope but read profile/email claims from/userinfomust now request those scopes explicitly.Migration for clients:
openid→ receive onlysubopenid email→ receivesub,email,email_verifiedopenid profile→ receivesub+ all profile claim keys (values may be JSONnullwhen the user has no value for that claim — keys are present so callers have a stable schema)openid profile email→ receive both groupsSee
docs/oauth2-oidc-endpoints.mdfor the full scope→claim mapping and example responses.Other Changes
Fixed (non-breaking)
at_hash— previously set to the nonce value in the implicit/token branch instead ofbase64url(sha256(access_token)[:16]). Now correctly computed for all flows per OIDC Core §3.2.2.10. Regression test asserts the exact computed value and that it is not equal to the nonce.nonceecho — now returned in the ID token whenever supplied in the auth request, regardless of flow (OIDC Core §2).registration_endpoint— removed. Previously pointed at/app(the signup HTML UI); spec-compliant OIDC clients interpret this field as an RFC 7591 dynamic client registration endpoint and would fail. Will return when RFC 7591 is actually implemented.Added (non-breaking)
grant_types_supported— now includesimplicitto honestly reflect that/authorizeacceptsresponse_type=tokenandresponse_type=id_token.Test plan
TestIDTokenClaimsCompliance— 3 sub-tests covering at_hash correctness, nonce echo, c_hash absenceTestUserInfoScopeFiltering— 5 sub-tests: only openid → only sub; openid+email; openid+profile; openid+profile+email; unknown custom scopeTestOpenIDDiscoveryCompliance— new sub-tests forgrant_types_supported_includes_implicitandregistration_endpoint_absentTestUserInfoEndpointCompliancestill green (error paths unchanged)make test-sqlitegreenOut of scope / follow-ups
id_token_hint+statepolish on/logout,prompt/max_age/login_hinton/authorize,auth_time/acr/amrID token claims