fix(oauth): support passing prompt, access_type, type_hint and additional params when constructing authorization URL (#1888)

This commit is contained in:
Waleed Latif
2025-03-20 08:56:13 +03:00
committed by GitHub
parent fa2cd17ff7
commit 3d36afe982
11 changed files with 120 additions and 51 deletions
@@ -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;
}
+15
View File
@@ -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 thats 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 }) {