documenso/packages/auth/server/lib/utils/handle-oauth-authorize-url.ts
Lucas Smith 7a583aa7af
fix: preserve prompt parameter in OAuth authorize URL builder (#2421)
The prompt option was being discarded for OAuth authorize URLs after
adding support for the NEXT_PRIVATE_OIDC_PROMPT env var. This meant
select_account (used elsewhere) was not being passed through.

Now defaults prompt to the provided option (or 'login'), and only
overwrites it when a valid OIDC prompt env var is set. Also adds a
type guard to validate the env var value.
2026-01-27 20:25:16 +11:00

101 lines
2.7 KiB
TypeScript

import { CodeChallengeMethod, OAuth2Client, generateCodeVerifier, generateState } from 'arctic';
import type { Context } from 'hono';
import { setCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { OAuthClientOptions } from '../../config';
import { sessionCookieOptions } from '../session/session-cookies';
import { getOpenIdConfiguration } from './open-id';
type HandleOAuthAuthorizeUrlOptions = {
/**
* Hono context.
*/
c: Context;
/**
* OAuth client options.
*/
clientOptions: OAuthClientOptions;
/**
* Optional redirect path to redirect the user somewhere on the app after authorization.
*/
redirectPath?: string;
/**
* Optional prompt to pass to the authorization endpoint.
*/
prompt?: 'none' | 'login' | 'consent' | 'select_account';
};
const isOidcPrompt = (value: unknown): value is HandleOAuthAuthorizeUrlOptions['prompt'] => {
return value === 'none' || value === 'login' || value === 'consent' || value === 'select_account';
};
const oauthCookieMaxAge = 60 * 10; // 10 minutes.
export const handleOAuthAuthorizeUrl = async (options: HandleOAuthAuthorizeUrlOptions) => {
const { c, clientOptions, redirectPath } = options;
let prompt = options.prompt ?? 'login';
if (!clientOptions.clientId || !clientOptions.clientSecret) {
throw new AppError(AppErrorCode.NOT_SETUP);
}
const { authorization_endpoint } = await getOpenIdConfiguration(clientOptions.wellKnownUrl, {
requiredScopes: clientOptions.scope,
});
const oAuthClient = new OAuth2Client(
clientOptions.clientId,
clientOptions.clientSecret,
clientOptions.redirectUrl,
);
const scopes = clientOptions.scope;
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = oAuthClient.createAuthorizationURLWithPKCE(
authorization_endpoint,
state,
CodeChallengeMethod.S256,
codeVerifier,
scopes,
);
// Pass the prompt to the authorization endpoint.
if (process.env.NEXT_PRIVATE_OIDC_PROMPT && isOidcPrompt(process.env.NEXT_PRIVATE_OIDC_PROMPT)) {
prompt = process.env.NEXT_PRIVATE_OIDC_PROMPT;
}
url.searchParams.set('prompt', prompt);
setCookie(c, `${clientOptions.id}_oauth_state`, state, {
...sessionCookieOptions,
sameSite: 'lax',
maxAge: oauthCookieMaxAge,
});
setCookie(c, `${clientOptions.id}_code_verifier`, codeVerifier, {
...sessionCookieOptions,
sameSite: 'lax',
maxAge: oauthCookieMaxAge,
});
if (redirectPath) {
setCookie(c, `${clientOptions.id}_redirect_path`, `${state} ${redirectPath}`, {
...sessionCookieOptions,
sameSite: 'lax',
maxAge: oauthCookieMaxAge,
});
}
return c.json({
redirectUrl: url.toString(),
});
};