From 3d36afe98252d15e53e2392818f5d4eeecdd7a55 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 19 Mar 2025 22:56:13 -0700 Subject: [PATCH] fix(oauth): support passing prompt, access_type, type_hint and additional params when constructing authorization URL (#1888) --- .../better-auth/src/api/routes/sign-in.ts | 10 +++++ .../src/oauth2/create-authorization-url.ts | 40 ++++++++++++++----- packages/better-auth/src/oauth2/types.ts | 15 +++++++ .../src/plugins/generic-oauth/index.ts | 30 ++++++++------ .../better-auth/src/social-providers/apple.ts | 21 +++++----- .../src/social-providers/facebook.ts | 3 +- .../src/social-providers/github.ts | 3 +- .../src/social-providers/gitlab.ts | 2 + .../src/social-providers/google.ts | 31 +++++++++----- .../src/social-providers/linkedin.ts | 8 +++- .../social-providers/microsoft-entra-id.ts | 8 +--- 11 files changed, 120 insertions(+), 51 deletions(-) diff --git a/packages/better-auth/src/api/routes/sign-in.ts b/packages/better-auth/src/api/routes/sign-in.ts index 5835f40fb2..423cbdc92a 100644 --- a/packages/better-auth/src/api/routes/sign-in.ts +++ b/packages/better-auth/src/api/routes/sign-in.ts @@ -132,6 +132,15 @@ export const signInSocial = createAuthEndpoint( "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider", }) .optional(), + /** + * The login hint to use for the authorization code request + */ + loginHint: z + .string({ + description: + "The login hint to use for the authorization code request", + }) + .optional(), }), metadata: { openapi: { @@ -271,6 +280,7 @@ export const signInSocial = createAuthEndpoint( codeVerifier, redirectURI: `${c.context.baseURL}/callback/${provider.id}`, scopes: c.body.scopes, + loginHint: c.body.loginHint, }); return c.json({ diff --git a/packages/better-auth/src/oauth2/create-authorization-url.ts b/packages/better-auth/src/oauth2/create-authorization-url.ts index 5ee8c2a0a6..5a02bd8c6f 100644 --- a/packages/better-auth/src/oauth2/create-authorization-url.ts +++ b/packages/better-auth/src/oauth2/create-authorization-url.ts @@ -12,6 +12,14 @@ export async function createAuthorizationURL({ redirectURI, duration, prompt, + accessType, + responseType, + display, + loginHint, + hd, + responseMode, + additionalParams, + scopeJoiner, }: { id: string; options: ProviderOptions; @@ -22,15 +30,29 @@ export async function createAuthorizationURL({ scopes: string[]; claims?: string[]; duration?: string; - prompt?: boolean; + prompt?: string; + accessType?: string; + responseType?: string; + display?: string; + loginHint?: string; + hd?: string; + responseMode?: string; + additionalParams?: Record; + scopeJoiner?: string; }) { const url = new URL(authorizationEndpoint); - url.searchParams.set("response_type", "code"); + url.searchParams.set("response_type", responseType || "code"); url.searchParams.set("client_id", options.clientId); url.searchParams.set("state", state); - url.searchParams.set("scope", scopes.join(" ")); + url.searchParams.set("scope", scopes.join(scopeJoiner || " ")); url.searchParams.set("redirect_uri", options.redirectURI || redirectURI); - + duration && url.searchParams.set("duration", duration); + display && url.searchParams.set("display", display); + loginHint && url.searchParams.set("login_hint", loginHint); + prompt && url.searchParams.set("prompt", prompt); + hd && url.searchParams.set("hd", hd); + accessType && url.searchParams.set("access_type", accessType); + responseMode && url.searchParams.set("response_mode", responseMode); if (codeVerifier) { const codeChallenge = await generateCodeChallenge(codeVerifier); url.searchParams.set("code_challenge_method", "S256"); @@ -51,12 +73,10 @@ export async function createAuthorizationURL({ }), ); } - if (duration) { - url.searchParams.set("duration", duration); + if (additionalParams) { + Object.entries(additionalParams).forEach(([key, value]) => { + url.searchParams.set(key, value); + }); } - if (prompt) { - url.searchParams.set("prompt", "select_account"); - } - return url; } diff --git a/packages/better-auth/src/oauth2/types.ts b/packages/better-auth/src/oauth2/types.ts index a60d662e2c..78a56d8bdb 100644 --- a/packages/better-auth/src/oauth2/types.ts +++ b/packages/better-auth/src/oauth2/types.ts @@ -19,6 +19,8 @@ export interface OAuthProvider< codeVerifier: string; scopes?: string[]; redirectURI: string; + display?: string; + loginHint?: string; }) => Promise | URL; name: string; validateAuthorizationCode: (data: { @@ -139,4 +141,17 @@ export type ProviderOptions = any> = { * Disable sign up for new users. */ disableSignUp?: boolean; + /** + * The prompt to use for the authorization code request + */ + prompt?: + | "select_account" + | "consent" + | "login" + | "none" + | "select_account+consent"; + /** + * The response mode to use for the authorization code request + */ + responseMode?: "query" | "form_post"; }; diff --git a/packages/better-auth/src/plugins/generic-oauth/index.ts b/packages/better-auth/src/plugins/generic-oauth/index.ts index d709726496..9717b6bb6e 100644 --- a/packages/better-auth/src/plugins/generic-oauth/index.ts +++ b/packages/better-auth/src/plugins/generic-oauth/index.ts @@ -60,6 +60,11 @@ interface GenericOAuthConfig { * @default "code" */ responseType?: string; + /** + * The response mode to use for the authorization code request. + + */ + responseMode?: "query" | "form_post"; /** * Prompt parameter for the authorization request. * Controls the authentication experience for the user. @@ -364,6 +369,7 @@ export const genericOAuth = (options: GenericOAuthOptions) => { prompt, accessType, authorizationUrlParams, + responseMode, } = config; let finalAuthUrl = authorizationUrl; let finalTokenUrl = tokenUrl; @@ -413,20 +419,12 @@ export const genericOAuth = (options: GenericOAuthOptions) => { ? [...ctx.body.scopes, ...(scopes || [])] : scopes || [], redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerId}`, + prompt, + accessType, + responseType, + responseMode, + additionalParams: authorizationUrlParams, }); - - if (responseType && responseType !== "code") { - authUrl.searchParams.set("response_type", responseType); - } - - if (prompt) { - authUrl.searchParams.set("prompt", prompt); - } - - if (accessType) { - authUrl.searchParams.set("access_type", accessType); - } - return ctx.json({ url: authUrl.toString(), redirect: !ctx.body.disableRedirect, @@ -694,6 +692,9 @@ export const genericOAuth = (options: GenericOAuthOptions) => { discoveryUrl, pkce, scopes, + prompt, + accessType, + authorizationUrlParams, } = provider; let finalAuthUrl = authorizationUrl; @@ -742,6 +743,9 @@ export const genericOAuth = (options: GenericOAuthOptions) => { codeVerifier: pkce ? state.codeVerifier : undefined, scopes: scopes || [], redirectURI: `${c.context.baseURL}/oauth2/callback/${providerId}`, + prompt, + accessType, + additionalParams: authorizationUrlParams, }); return c.json({ diff --git a/packages/better-auth/src/social-providers/apple.ts b/packages/better-auth/src/social-providers/apple.ts index ed916682ff..80bf554da7 100644 --- a/packages/better-auth/src/social-providers/apple.ts +++ b/packages/better-auth/src/social-providers/apple.ts @@ -2,7 +2,7 @@ import { betterFetch } from "@better-fetch/fetch"; import { APIError } from "better-call"; import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; -import { validateAuthorizationCode } from "../oauth2"; +import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2"; export interface AppleProfile { /** * The subject registered claim identifies the principal that’s the subject @@ -73,17 +73,20 @@ export const apple = (options: AppleOptions) => { return { id: "apple", name: "Apple", - createAuthorizationURL({ state, scopes, redirectURI }) { + async createAuthorizationURL({ state, scopes, redirectURI }) { const _scope = options.disableDefaultScope ? [] : ["email", "name"]; options.scope && _scope.push(...options.scope); scopes && _scope.push(...scopes); - return new URL( - `https://appleid.apple.com/auth/authorize?client_id=${ - options.clientId - }&response_type=code&redirect_uri=${ - options.redirectURI || redirectURI - }&scope=${_scope.join(" ")}&state=${state}&response_mode=form_post`, - ); + const url = await createAuthorizationURL({ + id: "apple", + options, + authorizationEndpoint: "https://appleid.apple.com/auth/authorize", + scopes: _scope, + state, + redirectURI, + responseMode: "form_post", + }); + return url; }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { return validateAuthorizationCode({ diff --git a/packages/better-auth/src/social-providers/facebook.ts b/packages/better-auth/src/social-providers/facebook.ts index 82558eca0f..6a7318aa85 100644 --- a/packages/better-auth/src/social-providers/facebook.ts +++ b/packages/better-auth/src/social-providers/facebook.ts @@ -31,7 +31,7 @@ export const facebook = (options: FacebookOptions) => { return { id: "facebook", name: "Facebook", - async createAuthorizationURL({ state, scopes, redirectURI }) { + async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) { const _scopes = options.disableDefaultScope ? [] : ["email", "public_profile"]; @@ -44,6 +44,7 @@ export const facebook = (options: FacebookOptions) => { scopes: _scopes, state, redirectURI, + loginHint, }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { diff --git a/packages/better-auth/src/social-providers/github.ts b/packages/better-auth/src/social-providers/github.ts index 3309cc769f..3f1cc493fd 100644 --- a/packages/better-auth/src/social-providers/github.ts +++ b/packages/better-auth/src/social-providers/github.ts @@ -55,7 +55,7 @@ export const github = (options: GithubOptions) => { return { id: "github", name: "GitHub", - createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { + createAuthorizationURL({ state, scopes, loginHint, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["read:user", "user:email"]; @@ -68,6 +68,7 @@ export const github = (options: GithubOptions) => { scopes: _scopes, state, redirectURI, + loginHint, }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { diff --git a/packages/better-auth/src/social-providers/gitlab.ts b/packages/better-auth/src/social-providers/gitlab.ts index e3c7d22575..a76fecf851 100644 --- a/packages/better-auth/src/social-providers/gitlab.ts +++ b/packages/better-auth/src/social-providers/gitlab.ts @@ -79,6 +79,7 @@ export const gitlab = (options: GitlabOptions) => { state, scopes, codeVerifier, + loginHint, redirectURI, }) => { const _scopes = options.disableDefaultScope ? [] : ["read_user"]; @@ -92,6 +93,7 @@ export const gitlab = (options: GitlabOptions) => { state, redirectURI, codeVerifier, + loginHint, }); }, validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => { diff --git a/packages/better-auth/src/social-providers/google.ts b/packages/better-auth/src/social-providers/google.ts index 8466dac4a1..3f7ae186d8 100644 --- a/packages/better-auth/src/social-providers/google.ts +++ b/packages/better-auth/src/social-providers/google.ts @@ -33,9 +33,17 @@ export interface GoogleProfile { } export interface GoogleOptions extends ProviderOptions { + /** + * The access type to use for the authorization code request + */ accessType?: "offline" | "online"; - prompt?: "none" | "consent" | "select_account"; + /** + * The display mode to use for the authorization code request + */ display?: "page" | "popup" | "touch" | "wap"; + /** + * The hosted domain of the user + */ hd?: string; } @@ -43,7 +51,14 @@ export const google = (options: GoogleOptions) => { return { id: "google", name: "Google", - async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { + async createAuthorizationURL({ + state, + scopes, + codeVerifier, + redirectURI, + loginHint, + display, + }) { if (!options.clientId || !options.clientSecret) { logger.error( "Client Id and Client Secret is required for Google. Make sure to provide them in the options.", @@ -66,14 +81,12 @@ export const google = (options: GoogleOptions) => { state, codeVerifier, redirectURI, + prompt: options.prompt, + accessType: options.accessType, + display: display || options.display, + loginHint, + hd: options.hd, }); - - options.accessType && - url.searchParams.set("access_type", options.accessType); - options.prompt && url.searchParams.set("prompt", options.prompt); - options.display && url.searchParams.set("display", options.display); - options.hd && url.searchParams.set("hd", options.hd); - return url; }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { diff --git a/packages/better-auth/src/social-providers/linkedin.ts b/packages/better-auth/src/social-providers/linkedin.ts index 948cda5738..a15105dd7c 100644 --- a/packages/better-auth/src/social-providers/linkedin.ts +++ b/packages/better-auth/src/social-providers/linkedin.ts @@ -26,7 +26,12 @@ export const linkedin = (options: LinkedInOptions) => { return { id: "linkedin", name: "Linkedin", - createAuthorizationURL: async ({ state, scopes, redirectURI }) => { + createAuthorizationURL: async ({ + state, + scopes, + redirectURI, + loginHint, + }) => { const _scopes = options.disableDefaultScope ? [] : ["profile", "email", "openid"]; @@ -38,6 +43,7 @@ export const linkedin = (options: LinkedInOptions) => { authorizationEndpoint, scopes: _scopes, state, + loginHint, redirectURI, }); }, diff --git a/packages/better-auth/src/social-providers/microsoft-entra-id.ts b/packages/better-auth/src/social-providers/microsoft-entra-id.ts index f337f89c99..77c5a21755 100644 --- a/packages/better-auth/src/social-providers/microsoft-entra-id.ts +++ b/packages/better-auth/src/social-providers/microsoft-entra-id.ts @@ -29,12 +29,6 @@ export interface MicrosoftOptions * Disable profile photo */ disableProfilePhoto?: boolean; - - /** - * Require user to select their account even if only one account is logged in - * @default false - */ - requireSelectAccount?: boolean; } export const microsoft = (options: MicrosoftOptions) => { @@ -58,7 +52,7 @@ export const microsoft = (options: MicrosoftOptions) => { codeVerifier: data.codeVerifier, scopes, redirectURI: data.redirectURI, - prompt: options.requireSelectAccount || false, + prompt: options.prompt, }); }, validateAuthorizationCode({ code, codeVerifier, redirectURI }) {