mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
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:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user