feat(google): add includeGrantedScopes option to opt out of incremental auth

Set `includeGrantedScopes: false` to omit `include_granted_scopes=true` from
Google's authorization URL so each OAuth flow requests only its own scopes.
Defaults to `true`, preserving current behavior.

Closes #9326

Co-authored-by: GautamBytes <161146829+GautamBytes@users.noreply.github.com>
This commit is contained in:
Taesu
2026-05-31 10:15:51 +01:00
committed by Gustavo Valverde
co-authored by GautamBytes
parent af95f0d305
commit c779dfc8fc
4 changed files with 116 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@better-auth/core": patch
---
Add `includeGrantedScopes` option to the Google provider. Set to `false` to omit `include_granted_scopes=true` from the authorization URL so each OAuth flow requests only its own scopes instead of accumulating prior grants. Defaults to `true`, preserving current behavior.
@@ -187,6 +187,22 @@ This will trigger a new OAuth flow that requests the additional scopes. After co
provider.
</Callout>
### Disable incremental authorization
By default, Better Auth sends `include_granted_scopes=true` to Google's authorization endpoint so newly issued access tokens cover scopes from prior grants in addition to the ones requested for the current flow. Set `includeGrantedScopes: false` if each OAuth flow should request only its own scopes.
```ts
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
includeGrantedScopes: false, // [!code highlight]
},
}
```
See Google's [Incremental authorization](https://developers.google.com/identity/protocols/oauth2/web-server#incrementalAuth) docs for the underlying parameter behavior.
### Always get refresh token
Google only issues a refresh token the first time a user consents to your app.
+78
View File
@@ -2652,3 +2652,81 @@ describe("account.scope path-dependent semantics", async () => {
);
});
});
/**
* @see https://github.com/better-auth/better-auth/issues/9326
*/
describe("Google Provider — includeGrantedScopes", async () => {
it("includes `include_granted_scopes=true` in the authorization URL by default", async () => {
const { client } = await getTestInstance(
{
socialProviders: {
google: {
clientId: "test",
clientSecret: "test",
},
},
},
{ disableTestUser: true },
);
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
});
const authUrl = new URL(signInRes.data!.url!);
expect(authUrl.searchParams.get("include_granted_scopes")).toBe("true");
});
it("includes `include_granted_scopes=true` when explicitly enabled", async () => {
const { client } = await getTestInstance(
{
socialProviders: {
google: {
clientId: "test",
clientSecret: "test",
includeGrantedScopes: true,
},
},
},
{ disableTestUser: true },
);
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
});
const authUrl = new URL(signInRes.data!.url!);
expect(authUrl.searchParams.get("include_granted_scopes")).toBe("true");
});
it("omits `include_granted_scopes` from the authorization URL when disabled", async () => {
const { client } = await getTestInstance(
{
socialProviders: {
google: {
clientId: "test",
clientSecret: "test",
includeGrantedScopes: false,
},
},
},
{ disableTestUser: true },
);
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
scopes: ["https://www.googleapis.com/auth/drive.file"],
});
const authUrl = new URL(signInRes.data!.url!);
expect(authUrl.searchParams.has("include_granted_scopes")).toBe(false);
expect(authUrl.searchParams.get("scope")).toContain("openid");
expect(authUrl.searchParams.get("scope")).toContain(
"https://www.googleapis.com/auth/drive.file",
);
});
});
+17 -3
View File
@@ -51,6 +51,17 @@ export interface GoogleOptions extends ProviderOptions<GoogleProfile> {
* The hosted domain of the user
*/
hd?: string | undefined;
/**
* Whether to send `include_granted_scopes=true` to Google's authorization
* endpoint, which lets new access tokens cover scopes from prior grants
* in addition to the ones requested for this flow. Set to `false` when
* each OAuth flow should request only its own scopes.
*
* Defaults to `true`.
*
* @see https://developers.google.com/identity/protocols/oauth2/web-server#incrementalAuth
*/
includeGrantedScopes?: boolean | undefined;
}
export const google = (options: GoogleOptions) => {
@@ -92,9 +103,12 @@ export const google = (options: GoogleOptions) => {
display: display || options.display,
loginHint,
hd: options.hd,
additionalParams: {
include_granted_scopes: "true",
},
additionalParams:
options.includeGrantedScopes === false
? undefined
: {
include_granted_scopes: "true",
},
});
return url;
},