[GH-ISSUE #7871] oauth-provider: consent with narrowed scope causes infinite redirect loop #19566

Closed
opened 2026-04-15 18:49:14 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @gustavovalverde on GitHub (Feb 9, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7871

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Configure oauthProvider with multiple custom scopes (e.g., proof:verification, identity.name)
  2. Register an OAuth client that requests both scope families
  3. On the consent page, submit consent with a subset of the originally-requested scopes via POST /oauth2/consent with { accept: true, scope: "openid email proof:verification" } (omitting identity.name from the original request)
  4. Observe: The user is redirected back to the consent page in an infinite loop instead of receiving an authorization code

Current vs. Expected behavior

Current behavior:

The consent endpoint (consent.ts) correctly accepts scope narrowing — it validates that the submitted scopes are a subset of the originally-requested scopes (line 32-38), and stores the narrowed scopes in the consent record (line 90).

However, after storing consent, it calls authorizeEndpoint() (line 121), which re-checks consent coverage at line 356-358:

if (
  !consent ||
  !requestedScopes.every((val) => consent.scopes.includes(val))
) {
  return redirectWithPromptCode(ctx, opts, "consent");
}

Since the stored consent has the narrowed scope set, and requestedScopes still contains the full original scope from the URL query parameter, this check fails and redirects back to consent — creating an infinite loop.

Expected behavior:

Per RFC 6749 §3.3: "If the issued access token scope is different from the one requested by the client, the authorization server MUST include the 'scope' response parameter to inform the client of the actual scope granted."

The authorization server should accept a narrowed consent and issue an authorization code with the narrowed scope. The token response should include the scope parameter reflecting the actual granted scope.

Proposed fix (two changes in authorize.ts):

  1. When consent exists but covers only a subset of requested scopes, narrow query.scope to match the consent and proceed to issue the authorization code
  2. This already works correctly downstream — the token endpoint reads query.scope from the stored verification value

What version of Better Auth are you using?

1.5.0-beta.13

System info

macOS, Node.js 24, pnpm 10

Which area(s) are affected? (Select all that apply)

Backend, Package

Auth config (if applicable)

oauthProvider({
  scopes: [
    "openid", "profile", "email",
    "proof:identity", "proof:verification", "proof:age",
    "identity.name", "identity.dob", "identity.address",
  ],
  consentPage: "/oauth/consent",
  loginPage: "/sign-in",
})

Additional context

Use case: We need users to consent to identity.* scopes each time (vault unlock required), while proof:* scopes can be auto-skipped. The consent page intentionally submits a narrower scope to avoid persisting identity scopes, but the authorize endpoint rejects this.

The consent endpoint already does the right thing (validates subset, stores narrowed scope). The fix is only needed in the authorize endpoint's post-consent check.

Originally created by @gustavovalverde on GitHub (Feb 9, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7871 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Configure `oauthProvider` with multiple custom scopes (e.g., `proof:verification`, `identity.name`) 2. Register an OAuth client that requests both scope families 3. On the consent page, submit consent with a **subset** of the originally-requested scopes via `POST /oauth2/consent` with `{ accept: true, scope: "openid email proof:verification" }` (omitting `identity.name` from the original request) 4. **Observe:** The user is redirected back to the consent page in an infinite loop instead of receiving an authorization code ### Current vs. Expected behavior **Current behavior:** The consent endpoint (`consent.ts`) correctly accepts scope narrowing — it validates that the submitted scopes are a subset of the originally-requested scopes (line 32-38), and stores the narrowed scopes in the consent record (line 90). However, after storing consent, it calls `authorizeEndpoint()` (line 121), which re-checks consent coverage at line 356-358: ```typescript if ( !consent || !requestedScopes.every((val) => consent.scopes.includes(val)) ) { return redirectWithPromptCode(ctx, opts, "consent"); } ``` Since the stored consent has the narrowed scope set, and `requestedScopes` still contains the full original scope from the URL query parameter, this check fails and redirects back to consent — creating an infinite loop. **Expected behavior:** Per RFC 6749 §3.3: *"If the issued access token scope is different from the one requested by the client, the authorization server MUST include the 'scope' response parameter to inform the client of the actual scope granted."* The authorization server should accept a narrowed consent and issue an authorization code with the narrowed scope. The token response should include the `scope` parameter reflecting the actual granted scope. **Proposed fix (two changes in `authorize.ts`):** 1. When consent exists but covers only a subset of requested scopes, narrow `query.scope` to match the consent and proceed to issue the authorization code 2. This already works correctly downstream — the token endpoint reads `query.scope` from the stored verification value ### What version of Better Auth are you using? 1.5.0-beta.13 ### System info macOS, Node.js 24, pnpm 10 ### Which area(s) are affected? (Select all that apply) Backend, Package ### Auth config (if applicable) ```typescript oauthProvider({ scopes: [ "openid", "profile", "email", "proof:identity", "proof:verification", "proof:age", "identity.name", "identity.dob", "identity.address", ], consentPage: "/oauth/consent", loginPage: "/sign-in", }) ``` ### Additional context **Use case:** We need users to consent to `identity.*` scopes each time (vault unlock required), while `proof:*` scopes can be auto-skipped. The consent page intentionally submits a narrower scope to avoid persisting identity scopes, but the authorize endpoint rejects this. The consent endpoint already does the right thing (validates subset, stores narrowed scope). The fix is only needed in the authorize endpoint's post-consent check.
GiteaMirror added the lockedbug labels 2026-04-15 18:49:14 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Feb 9, 2026):

It sounds like you're experiencing an infinite redirect loop with the consent process when narrowing scopes in your OAuth flow. From your description, the issue appears to stem from a misalignment between the scoped consent granted and the required scopes on the authorization endpoint.

Current Behavior

After narrowing the scopes and submitting consent, the authorization endpoint checks if all originally requested scopes are included in the consent. Since you reduced the scope to a subset, this check fails, leading to the observed infinite redirect loop.

Expected Behavior

As per RFC 6749, if a narrower scope is accepted, the authorization server should issue an authorization code with the reduced scope and reflect this in the response.

Proposed Fix

To resolve this, you can implement two changes in the authorize.ts file:

  1. Narrow the query.scope to match the consent, allowing for successful issuance of the authorization code.
  2. Ensure that stored consent can be processed correctly without reverting to the original required scopes.

This should help you avoid the infinite loop issue.

For additional support or discussion on this specific bug, please tag @better-auth-agent for further assistance from the team. Is there anything specific you would like more clarity on regarding these steps?

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3869565256 --> @better-auth-agent[bot] commented on GitHub (Feb 9, 2026): It sounds like you're experiencing an infinite redirect loop with the consent process when narrowing scopes in your OAuth flow. From your description, the issue appears to stem from a misalignment between the scoped consent granted and the required scopes on the authorization endpoint. ### Current Behavior After narrowing the scopes and submitting consent, the authorization endpoint checks if all originally requested scopes are included in the consent. Since you reduced the scope to a subset, this check fails, leading to the observed infinite redirect loop. ### Expected Behavior As per RFC 6749, if a narrower scope is accepted, the authorization server should issue an authorization code with the reduced scope and reflect this in the response. ### Proposed Fix To resolve this, you can implement two changes in the `authorize.ts` file: 1. Narrow the `query.scope` to match the consent, allowing for successful issuance of the authorization code. 2. Ensure that stored consent can be processed correctly without reverting to the original required scopes. This should help you avoid the infinite loop issue. For additional support or discussion on this specific bug, please tag @better-auth-agent for further assistance from the team. Is there anything specific you would like more clarity on regarding these steps? _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19566