From 31de86e4250c4b7a59e7237a914624188dcb5358 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 14 Feb 2025 16:01:16 +1100 Subject: [PATCH] feat: add oidc --- apps/remix/app/components/forms/signin.tsx | 12 +- packages/auth/client/index.ts | 19 +- packages/auth/server/config.ts | 29 +++ packages/auth/server/index.ts | 8 +- .../lib/utils/handle-oauth-authorize-url.ts | 86 ++++++ .../lib/utils/handle-oauth-callback-url.ts | 192 ++++++++++++++ packages/auth/server/lib/utils/open-id.ts | 44 ++++ packages/auth/server/routes/callback.ts | 26 ++ packages/auth/server/routes/google.ts | 244 ------------------ packages/auth/server/routes/oauth.ts | 37 +++ 10 files changed, 443 insertions(+), 254 deletions(-) create mode 100644 packages/auth/server/config.ts create mode 100644 packages/auth/server/lib/utils/handle-oauth-authorize-url.ts create mode 100644 packages/auth/server/lib/utils/handle-oauth-callback-url.ts create mode 100644 packages/auth/server/lib/utils/open-id.ts create mode 100644 packages/auth/server/routes/callback.ts delete mode 100644 packages/auth/server/routes/google.ts create mode 100644 packages/auth/server/routes/oauth.ts diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx index 9154aeb92..79edbf849 100644 --- a/apps/remix/app/components/forms/signin.tsx +++ b/apps/remix/app/components/forms/signin.tsx @@ -257,7 +257,9 @@ export const SignInForm = ({ const onSignInWithGoogleClick = async () => { try { - await authClient.google.signIn(); + await authClient.google.signIn({ + redirectPath, + }); } catch (err) { toast({ title: _(msg`An unknown error occurred`), @@ -271,11 +273,9 @@ export const SignInForm = ({ const onSignInWithOIDCClick = async () => { try { - // eslint-disable-next-line no-promise-executor-return - await new Promise((resolve) => setTimeout(resolve, 2000)); - // await signIn('oidc', { - // callbackUrl, - // }); + await authClient.oidc.signIn({ + redirectPath, + }); } catch (err) { toast({ title: _(msg`An unknown error occurred`), diff --git a/packages/auth/client/index.ts b/packages/auth/client/index.ts index de14b654e..b9c197d63 100644 --- a/packages/auth/client/index.ts +++ b/packages/auth/client/index.ts @@ -118,7 +118,10 @@ export class AuthClient { public google = { signIn: async ({ redirectPath }: { redirectPath?: string } = {}) => { - const response = await this.client['google'].authorize.$post({ json: { redirectPath } }); + const response = await this.client['oauth'].authorize.google.$post({ + json: { redirectPath }, + }); + await this.handleError(response); const data = await response.json(); @@ -129,6 +132,20 @@ export class AuthClient { } }, }; + + public oidc = { + signIn: async ({ redirectPath }: { redirectPath?: string } = {}) => { + const response = await this.client['oauth'].authorize.oidc.$post({ json: { redirectPath } }); + await this.handleError(response); + + const data = await response.json(); + + // Redirect to external OIDC provider URL. + if (data.redirectUrl) { + window.location.href = data.redirectUrl; + } + }, + }; } export const authClient = new AuthClient({ diff --git a/packages/auth/server/config.ts b/packages/auth/server/config.ts new file mode 100644 index 000000000..995272570 --- /dev/null +++ b/packages/auth/server/config.ts @@ -0,0 +1,29 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { env } from '@documenso/lib/utils/env'; + +export type OAuthClientOptions = { + id: string; + scope: string[]; + clientId: string; + clientSecret: string; + wellKnownUrl: string; + redirectUrl: string; +}; + +export const GoogleAuthOptions: OAuthClientOptions = { + id: 'google', + scope: ['openid', 'email', 'profile'], + clientId: env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') ?? '', + clientSecret: env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET') ?? '', + redirectUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/callback/google`, + wellKnownUrl: 'https://accounts.google.com/.well-known/openid-configuration', +}; + +export const OidcAuthOptions: OAuthClientOptions = { + id: 'oidc', + scope: ['openid', 'email', 'profile'], + clientId: env('NEXT_PRIVATE_OIDC_CLIENT_ID') ?? '', + clientSecret: env('NEXT_PRIVATE_OIDC_CLIENT_SECRET') ?? '', + redirectUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/callback/oidc`, + wellKnownUrl: env('NEXT_PRIVATE_OIDC_WELL_KNOWN') ?? '', +}; diff --git a/packages/auth/server/index.ts b/packages/auth/server/index.ts index 3c784bc89..5f79b963b 100644 --- a/packages/auth/server/index.ts +++ b/packages/auth/server/index.ts @@ -7,8 +7,9 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { setCsrfCookie } from './lib/session/session-cookies'; +import { callbackRoute } from './routes/callback'; import { emailPasswordRoute } from './routes/email-password'; -import { googleRoute } from './routes/google'; +import { oauthRoute } from './routes/oauth'; import { passkeyRoute } from './routes/passkey'; import { sessionRoute } from './routes/session'; import { signOutRoute } from './routes/sign-out'; @@ -42,9 +43,10 @@ export const auth = new Hono() }) .route('/', sessionRoute) .route('/', signOutRoute) + .route('/callback', callbackRoute) + .route('/oauth', oauthRoute) .route('/email-password', emailPasswordRoute) - .route('/passkey', passkeyRoute) - .route('/google', googleRoute); + .route('/passkey', passkeyRoute); /** * Handle errors. diff --git a/packages/auth/server/lib/utils/handle-oauth-authorize-url.ts b/packages/auth/server/lib/utils/handle-oauth-authorize-url.ts new file mode 100644 index 000000000..6c2df1a07 --- /dev/null +++ b/packages/auth/server/lib/utils/handle-oauth-authorize-url.ts @@ -0,0 +1,86 @@ +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; +}; + +const oauthCookieMaxAge = 60 * 10; // 10 minutes. + +export const handleOAuthAuthorizeUrl = async (options: HandleOAuthAuthorizeUrlOptions) => { + const { c, clientOptions, redirectPath } = options; + + 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, + ); + + // Allow user to select account during login. + url.searchParams.append('prompt', 'login'); + + 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(), + }); +}; diff --git a/packages/auth/server/lib/utils/handle-oauth-callback-url.ts b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts new file mode 100644 index 000000000..d13748900 --- /dev/null +++ b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts @@ -0,0 +1,192 @@ +import { OAuth2Client, decodeIdToken } from 'arctic'; +import type { Context } from 'hono'; +import { deleteCookie } from 'hono/cookie'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; +import { prisma } from '@documenso/prisma'; +import { UserSecurityAuditLogType } from '@documenso/prisma/client'; + +import type { OAuthClientOptions } from '../../config'; +import { AuthenticationErrorCode } from '../errors/error-codes'; +import { onAuthorize } from './authorizer'; +import { getOpenIdConfiguration } from './open-id'; + +type HandleOAuthCallbackUrlOptions = { + c: Context; + clientOptions: OAuthClientOptions; +}; + +export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOptions) => { + const { c, clientOptions } = options; + + if (!clientOptions.clientId || !clientOptions.clientSecret) { + throw new AppError(AppErrorCode.NOT_SETUP); + } + + const { token_endpoint } = await getOpenIdConfiguration(clientOptions.wellKnownUrl, { + requiredScopes: clientOptions.scope, + }); + + const oAuthClient = new OAuth2Client( + clientOptions.clientId, + clientOptions.clientSecret, + clientOptions.redirectUrl, + ); + + const requestMeta = c.get('requestMetadata'); + + const code = c.req.query('code'); + const state = c.req.query('state'); + + const storedState = deleteCookie(c, `${clientOptions.id}_oauth_state`); + const storedCodeVerifier = deleteCookie(c, `${clientOptions.id}_code_verifier`); + const storedRedirectPath = deleteCookie(c, `${clientOptions.id}_redirect_path`) ?? ''; + + if (!code || !storedState || state !== storedState || !storedCodeVerifier) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Invalid or missing state', + }); + } + + // eslint-disable-next-line prefer-const + let [redirectState, redirectPath] = storedRedirectPath.split(' '); + + if (redirectState !== storedState || !redirectPath) { + redirectPath = '/documents'; + } + + const tokens = await oAuthClient.validateAuthorizationCode( + token_endpoint, + code, + storedCodeVerifier, + ); + + const accessToken = tokens.accessToken(); + const accessTokenExpiresAt = tokens.accessTokenExpiresAt(); + const idToken = tokens.idToken(); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const claims = decodeIdToken(tokens.idToken()) as Record; + + const email = claims.email; + const name = claims.name; + const sub = claims.sub; + + if (typeof email !== 'string' || typeof name !== 'string' || typeof sub !== 'string') { + throw new AppError(AuthenticationErrorCode.InvalidRequest, { + message: 'Invalid claims', + }); + } + + if (claims.email_verified !== true) { + throw new AppError(AuthenticationErrorCode.UnverifiedEmail, { + message: 'Account email is not verified', + }); + } + + // Find the account if possible. + const existingAccount = await prisma.account.findFirst({ + where: { + provider: clientOptions.id, + providerAccountId: sub, + }, + include: { + user: true, + }, + }); + + // Directly log in user if account already exists. + if (existingAccount) { + await onAuthorize({ userId: existingAccount.user.id }, c); + + return c.redirect(redirectPath, 302); + } + + const userWithSameEmail = await prisma.user.findFirst({ + where: { + email: email, + }, + }); + + // Handle existing user but no account. + if (userWithSameEmail) { + await prisma.$transaction(async (tx) => { + await tx.account.create({ + data: { + type: 'oauth', + provider: clientOptions.id, + providerAccountId: sub, + access_token: accessToken, + expires_at: Math.floor(accessTokenExpiresAt.getTime() / 1000), + token_type: 'Bearer', + id_token: idToken, + userId: userWithSameEmail.id, + }, + }); + + // Log link event. + await tx.userSecurityAuditLog.create({ + data: { + userId: userWithSameEmail.id, + ipAddress: requestMeta.ipAddress, + userAgent: requestMeta.userAgent, + type: UserSecurityAuditLogType.ACCOUNT_SSO_LINK, + }, + }); + + // If account already exists in an unverified state, remove the password to ensure + // they cannot sign in since we cannot confirm the password was set by the user. + if (!userWithSameEmail.emailVerified) { + await tx.user.update({ + where: { + id: userWithSameEmail.id, + }, + data: { + emailVerified: new Date(), + password: null, // Todo: Check this + }, + }); + } + }); + + await onAuthorize({ userId: userWithSameEmail.id }, c); + + return c.redirect(redirectPath, 302); + } + + // Handle new user. + const createdUser = await prisma.$transaction(async (tx) => { + const user = await tx.user.create({ + data: { + email: email, + name: name, + emailVerified: new Date(), + }, + }); + + await tx.account.create({ + data: { + type: 'oauth', + provider: clientOptions.id, + providerAccountId: sub, + access_token: accessToken, + expires_at: Math.floor(accessTokenExpiresAt.getTime() / 1000), + token_type: 'Bearer', + id_token: idToken, + userId: user.id, + }, + }); + + return user; + }); + + await onCreateUserHook(createdUser).catch((err) => { + // Todo: Add logging. + console.error(err); + }); + + await onAuthorize({ userId: createdUser.id }, c); + + return c.redirect(redirectPath, 302); +}; diff --git a/packages/auth/server/lib/utils/open-id.ts b/packages/auth/server/lib/utils/open-id.ts new file mode 100644 index 000000000..082126ea1 --- /dev/null +++ b/packages/auth/server/lib/utils/open-id.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +const ZOpenIdConfigurationSchema = z.object({ + authorization_endpoint: z.string(), + token_endpoint: z.string(), + scopes_supported: z.array(z.string()).optional(), +}); + +type OpenIdConfiguration = z.infer; + +type GetOpenIdConfigurationOptions = { + requiredScopes?: string[]; +}; + +export const getOpenIdConfiguration = async ( + wellKnownUrl: string, + options: GetOpenIdConfigurationOptions = {}, +): Promise => { + const response = await fetch(wellKnownUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch OIDC configuration: ${response.statusText}`); + } + + const rawConfig = await response.json(); + + const config = ZOpenIdConfigurationSchema.parse(rawConfig); + + // Validate required endpoints + if (!config.authorization_endpoint) { + throw new Error('Missing authorization_endpoint in OIDC configuration'); + } + + const supportedScopes = config.scopes_supported ?? []; + const requiredScopes = options.requiredScopes ?? []; + + const unsupportedScopes = requiredScopes.filter((scope) => !supportedScopes.includes(scope)); + + if (unsupportedScopes.length > 0) { + throw new Error(`Requested scopes not supported by provider: ${unsupportedScopes.join(', ')}`); + } + + return config; +}; diff --git a/packages/auth/server/routes/callback.ts b/packages/auth/server/routes/callback.ts new file mode 100644 index 000000000..07e762b47 --- /dev/null +++ b/packages/auth/server/routes/callback.ts @@ -0,0 +1,26 @@ +import { Hono } from 'hono'; + +import { GoogleAuthOptions, OidcAuthOptions } from '../config'; +import { handleOAuthCallbackUrl } from '../lib/utils/handle-oauth-callback-url'; +import type { HonoAuthContext } from '../types/context'; + +// Todo: Test +// api/auth/callback/google? +// api/auth/callback/oidc + +/** + * Have to create this route instead of bundling callback with oauth routes to provide + * backwards compatibility for self-hosters (since we used to use NextAuth). + */ +export const callbackRoute = new Hono() + /** + * OIDC callback verification. + */ + .get('/oidc', async (c) => handleOAuthCallbackUrl({ c, clientOptions: OidcAuthOptions })) + + /** + * Google callback verification. + * + * Todo: Double check this is the correct callback. + */ + .get('/google', async (c) => handleOAuthCallbackUrl({ c, clientOptions: GoogleAuthOptions })); diff --git a/packages/auth/server/routes/google.ts b/packages/auth/server/routes/google.ts deleted file mode 100644 index 49f447262..000000000 --- a/packages/auth/server/routes/google.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { sValidator } from '@hono/standard-validator'; -import { Google, decodeIdToken, generateCodeVerifier, generateState } from 'arctic'; -import { Hono } from 'hono'; -import { deleteCookie, setCookie } from 'hono/cookie'; -import { z } from 'zod'; - -import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; -import { env } from '@documenso/lib/utils/env'; -import { prisma } from '@documenso/prisma'; -import { UserSecurityAuditLogType } from '@documenso/prisma/client'; - -import { AuthenticationErrorCode } from '../lib/errors/error-codes'; -import { sessionCookieOptions } from '../lib/session/session-cookies'; -import { onAuthorize } from '../lib/utils/authorizer'; -import type { HonoAuthContext } from '../types/context'; - -const options = { - clientId: env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') ?? '', - clientSecret: env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET') ?? '', - redirectUri: `${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/google/callback`, - scope: ['openid', 'email', 'profile'], - id: 'google', -}; - -const google = new Google(options.clientId, options.clientSecret, options.redirectUri); - -// todo: NEXT_PRIVATE_OIDC_WELL_KNOWN??? - -const ZGoogleAuthorizeSchema = z.object({ - redirectPath: z.string().optional(), -}); - -export const googleRoute = new Hono() - /** - * Authorize endpoint. - */ - .post('/authorize', sValidator('json', ZGoogleAuthorizeSchema), (c) => { - const scopes = options.scope; - const state = generateState(); - - const codeVerifier = generateCodeVerifier(); - const url = google.createAuthorizationURL(state, codeVerifier, scopes); - - const { redirectPath } = c.req.valid('json'); - - setCookie(c, 'google_oauth_state', state, { - ...sessionCookieOptions, - sameSite: 'lax', // Todo - maxAge: 60 * 10, // 10 minutes. - }); - - setCookie(c, 'google_code_verifier', codeVerifier, { - ...sessionCookieOptions, - sameSite: 'lax', // Todo - maxAge: 60 * 10, // 10 minutes. - }); - - if (redirectPath) { - setCookie(c, 'google_redirect_path', `${state}:${redirectPath}`, { - ...sessionCookieOptions, - sameSite: 'lax', // Todo - maxAge: 60 * 10, // 10 minutes. - }); - } - - return c.json({ - redirectUrl: url.toString(), - }); - }) - /** - * Google callback verification. - */ - .get('/callback', async (c) => { - const requestMeta = c.get('requestMetadata'); - - const code = c.req.query('code'); - const state = c.req.query('state'); - - const storedState = deleteCookie(c, 'google_oauth_state'); - const storedCodeVerifier = deleteCookie(c, 'google_code_verifier'); - const storedredirectPath = deleteCookie(c, 'google_redirect_path') ?? ''; - - if (!code || !storedState || state !== storedState || !storedCodeVerifier) { - throw new AppError(AppErrorCode.INVALID_REQUEST, { - message: 'Invalid or missing state', - }); - } - - // eslint-disable-next-line prefer-const - let [redirectState, redirectPath] = storedredirectPath.split(':'); - - if (redirectState !== storedState || !redirectPath) { - redirectPath = '/documents'; - } - - const tokens = await google.validateAuthorizationCode(code, storedCodeVerifier); - const accessToken = tokens.accessToken(); - const accessTokenExpiresAt = tokens.accessTokenExpiresAt(); - const idToken = tokens.idToken(); - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const claims = decodeIdToken(tokens.idToken()) as Record; - - const googleEmail = claims.email; - const googleEmailVerified = claims.email_verified; - const googleName = claims.name; - const googleSub = claims.sub; - - if ( - typeof googleEmail !== 'string' || - typeof googleName !== 'string' || - typeof googleSub !== 'string' - ) { - throw new AppError(AuthenticationErrorCode.InvalidRequest, { - message: 'Invalid google claims', - }); - } - - if (claims.email_verified !== true) { - throw new AppError(AuthenticationErrorCode.UnverifiedEmail, { - message: 'Account email is not verified', - }); - } - - // Find the account if possible. - const existingAccount = await prisma.account.findFirst({ - where: { - provider: 'google', - providerAccountId: googleSub, - }, - include: { - user: true, - }, - }); - - // Directly log in user if account already exists. - if (existingAccount) { - await onAuthorize({ userId: existingAccount.user.id }, c); - - return c.redirect(redirectPath, 302); - } - - const userWithSameEmail = await prisma.user.findFirst({ - where: { - email: googleEmail, - }, - }); - - // Handle existing user but no account. - if (userWithSameEmail) { - await prisma.$transaction(async (tx) => { - await tx.account.create({ - data: { - type: 'oauth', - provider: 'google', - providerAccountId: googleSub, - access_token: accessToken, - expires_at: Math.floor(accessTokenExpiresAt.getTime() / 1000), - token_type: 'Bearer', - id_token: idToken, - userId: userWithSameEmail.id, - }, - }); - - // Log link event. - await tx.userSecurityAuditLog.create({ - data: { - userId: userWithSameEmail.id, - ipAddress: requestMeta.ipAddress, - userAgent: requestMeta.userAgent, - type: UserSecurityAuditLogType.ACCOUNT_SSO_LINK, - }, - }); - - // If account already exists in an unverified state, remove the password to ensure - // they cannot sign in since we cannot confirm the password was set by the user. - if (!userWithSameEmail.emailVerified) { - await tx.user.update({ - where: { - id: userWithSameEmail.id, - }, - data: { - emailVerified: new Date(), - password: null, // Todo: Check this - }, - }); - } - - // Apparently incredibly rare case? So we whole account to unverified. - if (!googleEmailVerified) { - // Todo: Add logging. - - await tx.user.update({ - where: { - id: userWithSameEmail.id, - }, - data: { - emailVerified: null, - }, - }); - } - }); - - await onAuthorize({ userId: userWithSameEmail.id }, c); - - return c.redirect(redirectPath, 302); - } - - // Handle new user. - const createdUser = await prisma.$transaction(async (tx) => { - const user = await tx.user.create({ - data: { - email: googleEmail, - name: googleName, - }, - }); - - await tx.account.create({ - data: { - type: 'oauth', - provider: 'google', - providerAccountId: googleSub, - access_token: accessToken, - expires_at: Math.floor(accessTokenExpiresAt.getTime() / 1000), - token_type: 'Bearer', - id_token: idToken, - userId: user.id, - }, - }); - - return user; - }); - - await onCreateUserHook(createdUser).catch((err) => { - // Todo: Add logging. - console.error(err); - }); - - await onAuthorize({ userId: createdUser.id }, c); - - return c.redirect(redirectPath, 302); - }); diff --git a/packages/auth/server/routes/oauth.ts b/packages/auth/server/routes/oauth.ts new file mode 100644 index 000000000..0eed4fba1 --- /dev/null +++ b/packages/auth/server/routes/oauth.ts @@ -0,0 +1,37 @@ +import { sValidator } from '@hono/standard-validator'; +import { Hono } from 'hono'; +import { z } from 'zod'; + +import { GoogleAuthOptions, OidcAuthOptions } from '../config'; +import { handleOAuthAuthorizeUrl } from '../lib/utils/handle-oauth-authorize-url'; +import type { HonoAuthContext } from '../types/context'; + +const ZOAuthAuthorizeSchema = z.object({ + redirectPath: z.string().optional(), +}); + +export const oauthRoute = new Hono() + /** + * Google authorize endpoint. + */ + .post('/authorize/google', sValidator('json', ZOAuthAuthorizeSchema), async (c) => { + const { redirectPath } = c.req.valid('json'); + + return handleOAuthAuthorizeUrl({ + c, + clientOptions: GoogleAuthOptions, + redirectPath, + }); + }) + /** + * OIDC authorize endpoint. + */ + .post('/authorize/oidc', sValidator('json', ZOAuthAuthorizeSchema), async (c) => { + const { redirectPath } = c.req.valid('json'); + + return handleOAuthAuthorizeUrl({ + c, + clientOptions: OidcAuthOptions, + redirectPath, + }); + });