mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-31 20:05:38 -05:00
fix(oauth): support passing prompt, access_type, type_hint and additional params when constructing authorization URL (#1888)
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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<string, string>;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface OAuthProvider<
|
||||
codeVerifier: string;
|
||||
scopes?: string[];
|
||||
redirectURI: string;
|
||||
display?: string;
|
||||
loginHint?: string;
|
||||
}) => Promise<URL> | URL;
|
||||
name: string;
|
||||
validateAuthorizationCode: (data: {
|
||||
@@ -139,4 +141,17 @@ export type ProviderOptions<Profile extends Record<string, any> = 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";
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -33,9 +33,17 @@ export interface GoogleProfile {
|
||||
}
|
||||
|
||||
export interface GoogleOptions extends ProviderOptions<GoogleProfile> {
|
||||
/**
|
||||
* 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 }) => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
Reference in New Issue
Block a user