Skip to content

fix(auth): accept stored scopes over 256 characters in OAuth responses - #21569

Open
fxa-agent[bot] wants to merge 1 commit into
mainfrom
fxa-14812
Open

fxa-agent[bot] wants to merge 1 commit into
mainfrom
fxa-14812

Conversation

@fxa-agent

@fxa-agent fxa-agent Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Because

  • The /token and /oauth/token response schemas used the request scope validator. That validator caps the scope at 256 characters.
  • A stored scope can be longer than the request cap. The response check then fails.
  • FXA-14756 fixed the same problem for /introspect with an inline validator.

This pull request

  • Adds storedScopeString (Joi.string().allow('')) to lib/oauth/validators.js. It is for responses only.
  • Uses it for the five response scope fields in lib/routes/oauth/token.js.
  • Uses it in lib/routes/oauth/introspect.js in place of the inline validator.
  • Adds response-schema tests in token.spec.ts and storedScopeString tests in validators.spec.ts.
  • Does not change the request scope validators.

Issue that this pull request solves

Closes: https://mozilla-hub.atlassian.net/browse/FXA-14812

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on: lib/oauth/validators.js and the response schemas in lib/routes/oauth/token.js.
  • Suggested review order: validator, token.js, introspect.js, specs.
  • Risky or complex parts: none in the diff. See the open question below.

Screenshots (Optional)

Other information (Optional)

Where the validator lives. The ticket suggested lib/routes/validators.js. token.js and introspect.js import lib/oauth/validators.js, so the validator is next to the OAuth scope validator.

Not changed on purpose. The /authorization response scope fields (authorization.js lines 605 and 694) use an inline Joi.string() with no length cap. They do not fail on a long scope. I left them for a follow-up.

Tests I ran (/fxa-verify --plan), all PASS:

  • Unit specs, integration spec test/remote/oauth_token_route.in.spec.ts, eslint, prettier, and tsc with tsconfig.build.json.
  • A check on the local stack: sign up, verify, then POST /v1/oauth/token (fxa-credentials grant, Settings public client). It returned scope=profile token_type=bearer refresh=true.
  • /fxa-verify --revert on token.spec.ts: the four new response-schema cases fail without the fix and pass with it.
  • The wider unit run covered the sibling specs only. 68 related specs did not run.

Not run. The 123done sign-in spec tests/oauth/signin.spec.ts could not run here. The token exchange failed with errno 109 because the sandbox has no packages/123done/secrets.json. CI covers it.

Not reachable over HTTP locally. A scope over 256 characters did not occur on the local stack. The long-scope case is covered only by the unit specs.

Question for the reviewer: does this fix the reported 500 on its own? I traced how a scope can pass 256 characters:

  • The request scope is capped at 256.
  • The refresh_token grant replaces the scope with params.scope, so it stays at or under 256.
  • Authorization codes are stored in codes.scope VARCHAR(256).
  • Token exchange builds combinedScope = subjectToken.scope.union(requestedScope) (token.js line 618). That scope can reach about 512 characters.
  • generateTokens in lib/oauth/grant.js writes the access token to Redis first. It then inserts the refresh token into refreshTokens.scope VARCHAR(256). The MySQL connection uses STRICT_ALL_TABLES, so that insert should fail before the response schema runs. This would leave an orphan access token in Redis.

The token exchange path may be a third site that needs a fix. I changed no code for this.

Because:
- The /token and /oauth/token response schemas used the 256-char request scope validator.
- A stored access-token scope can be longer, so the response check fails.

This commit:
- Adds a response-only storedScopeString validator to lib/oauth/validators.js.
- Uses it for every response scope in token.js and in introspect.js.
- Adds response-schema and validator specs for a scope over 256 chars.

Closes FXA-14812
@fxa-agent
fxa-agent Bot requested a review from a team as a code owner October 20, 2026 18:17
@fxa-agent fxa-agent Bot added the auto label Oct 20, 2026
@vbudhram
vbudhram requested a lite review from Copilot October 20, 2026 18:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Token exchange may fail at the database persistence boundary before response validation runs.

1 open issue
What changed in this PR

This PR updates OAuth response validation to accept stored scopes longer than 256 characters without changing request validation.

Changes:

  • Adds the storedScopeString response validator.
  • Applies it to token and introspection responses.
  • Adds validator and response-schema tests.
File Summary
packages/​fxa-auth-server/​lib/​routes/​oauth/​token.spec.ts Adds long-scope response tests.
packages/​fxa-auth-server/​lib/​routes/​oauth/​token.js Uses the stored-scope validator; token-exchange persistence remains unresolved.
packages/​fxa-auth-server/​lib/​routes/​oauth/​introspect.js Reuses the stored-scope validator.
packages/​fxa-auth-server/​lib/​oauth/​validators.spec.ts Tests long and empty stored scopes.
packages/​fxa-auth-server/​lib/​oauth/​validators.js Defines the response-only stored-scope validator.

Review effort: Lite

access_token: validators.accessToken.required(),
refresh_token: validators.refreshToken.required(),
scope: validators.scope.required(),
scope: validators.storedScopeString.required(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: Out of scope: the refresh token column cap needs a DB migration or a product decision, so it goes to a follow-up ticket

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants