From da875e451cdad38b46ca048e1d963dcec4fc3e78 Mon Sep 17 00:00:00 2001 From: diced Date: Mon, 25 Nov 2024 22:36:55 -0800 Subject: [PATCH] fix: oauth state is encrypted --- src/lib/oauth/providerUtil.ts | 8 ++++---- src/lib/oauth/withOAuth.ts | 11 +++++++++-- src/pages/api/auth/oauth/discord.ts | 10 +++++++--- src/pages/api/auth/oauth/github.ts | 10 +++++++--- src/pages/api/auth/oauth/google.ts | 10 +++++++--- src/pages/api/auth/oauth/oidc.ts | 10 +++++++--- 6 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/lib/oauth/providerUtil.ts b/src/lib/oauth/providerUtil.ts index a8e52aee..0ccb785d 100755 --- a/src/lib/oauth/providerUtil.ts +++ b/src/lib/oauth/providerUtil.ts @@ -11,7 +11,7 @@ export function findProvider( export const githubAuth = { url: (clientId: string, state?: string, redirectUri?: string) => `https://github.com/login/oauth/authorize?client_id=${clientId}&scope=read:user${ - state ? `&state=${state}` : '' + state ? `&state=${encodeURIComponent(state)}` : '' }${redirectUri ? `&redirect_uri=${encodeURIComponent(redirectUri)}` : ''}`, user: async (accessToken: string) => { const res = await fetch('https://api.github.com/user', { @@ -29,7 +29,7 @@ export const discordAuth = { url: (clientId: string, origin: string, state?: string, redirectUri?: string) => `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent( redirectUri ?? `${origin}/api/auth/oauth/discord`, - )}&response_type=code&scope=identify${state ? `&state=${state}` : ''}`, + )}&response_type=code&scope=identify${state ? `&state=${encodeURIComponent(state)}` : ''}`, user: async (accessToken: string) => { const res = await fetch('https://discord.com/api/users/@me', { headers: { @@ -47,7 +47,7 @@ export const googleAuth = { `https://accounts.google.com/o/oauth2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent( redirectUri ?? `${origin}/api/auth/oauth/google`, )}&response_type=code&access_type=offline&scope=https://www.googleapis.com/auth/userinfo.profile${ - state ? `&state=${state}` : '' + state ? `&state=${encodeURIComponent(state)}` : '' }`, user: async (accessToken: string) => { const res = await fetch('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', { @@ -65,7 +65,7 @@ export const oidcAuth = { url: (clientId: string, origin: string, authorizeUrl: string, state?: string, redirectUri?: string) => `${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent( redirectUri ?? `${origin}/api/auth/oauth/oidc`, - )}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${state}` : ''}`, + )}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${encodeURIComponent(state)}` : ''}`, user: async (accessToken: string, userInfoUrl: string) => { const res = await fetch(userInfoUrl, { headers: { diff --git a/src/lib/oauth/withOAuth.ts b/src/lib/oauth/withOAuth.ts index b754db29..3959d632 100755 --- a/src/lib/oauth/withOAuth.ts +++ b/src/lib/oauth/withOAuth.ts @@ -2,7 +2,7 @@ import { NextApiReq, NextApiRes } from '@/lib/response'; import { OAuthProviderType } from '@prisma/client'; import { prisma } from '../db'; import { findProvider } from './providerUtil'; -import { createToken } from '../crypto'; +import { createToken, decrypt } from '../crypto'; import { config } from '../config'; import { User } from '../db/models/user'; import Logger, { log } from '../logger'; @@ -88,7 +88,14 @@ export const withOAuth = const userOauth = findProvider(provider, user?.oauthProviders ?? []); - if (state === 'link') { + let urlState; + try { + urlState = decrypt(decodeURIComponent(state ?? ''), config.core.secret); + } catch { + urlState = null; + } + + if (urlState === 'link') { if (!user) return res.unauthorized('invalid session'); if (findProvider(provider, user.oauthProviders)) diff --git a/src/pages/api/auth/oauth/discord.ts b/src/pages/api/auth/oauth/discord.ts index ba329e93..db49bdf6 100755 --- a/src/pages/api/auth/oauth/discord.ts +++ b/src/pages/api/auth/oauth/discord.ts @@ -6,8 +6,9 @@ import enabled from '@/lib/oauth/enabled'; import { discordAuth } from '@/lib/oauth/providerUtil'; import { fetchToDataURL } from '@/lib/base64'; import Logger from '@/lib/logger'; +import { encrypt } from '@/lib/crypto'; -async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise { +async function handler({ code, host }: OAuthQuery, logger: Logger): Promise { if (!config.features.oauthRegistration) return { error: 'OAuth registration is disabled.', @@ -22,15 +23,18 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi error_code: 401, }; - if (!code) + if (!code) { + const linkState = encrypt('link', config.core.secret); + return { redirect: discordAuth.url( config.oauth.discord.clientId!, `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`, - state, + linkState, config.oauth.discord.redirectUri ?? undefined, ), }; + } const body = new URLSearchParams({ client_id: config.oauth.discord.clientId!, diff --git a/src/pages/api/auth/oauth/github.ts b/src/pages/api/auth/oauth/github.ts index 918a1599..9b35be9c 100755 --- a/src/pages/api/auth/oauth/github.ts +++ b/src/pages/api/auth/oauth/github.ts @@ -1,5 +1,6 @@ import { fetchToDataURL } from '@/lib/base64'; import { config } from '@/lib/config'; +import { encrypt } from '@/lib/crypto'; import Logger from '@/lib/logger'; import { combine } from '@/lib/middleware/combine'; import { method } from '@/lib/middleware/method'; @@ -7,7 +8,7 @@ import enabled from '@/lib/oauth/enabled'; import { githubAuth } from '@/lib/oauth/providerUtil'; import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth'; -async function handler({ code, state }: OAuthQuery, logger: Logger): Promise { +async function handler({ code }: OAuthQuery, logger: Logger): Promise { if (!config.features.oauthRegistration) return { error: 'OAuth registration is disabled.', @@ -22,14 +23,17 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise { +async function handler({ code, host }: OAuthQuery, _logger: Logger): Promise { if (!config.features.oauthRegistration) return { error: 'OAuth registration is disabled.', @@ -22,15 +23,18 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom error_code: 401, }; - if (!code) + if (!code) { + const linkState = encrypt('link', config.core.secret); + return { redirect: googleAuth.url( config.oauth.google.clientId!, `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`, - state, + linkState, config.oauth.google.redirectUri ?? undefined, ), }; + } const body = new URLSearchParams({ client_id: config.oauth.google.clientId!, diff --git a/src/pages/api/auth/oauth/oidc.ts b/src/pages/api/auth/oauth/oidc.ts index 0822f9f7..7bfb9b51 100644 --- a/src/pages/api/auth/oauth/oidc.ts +++ b/src/pages/api/auth/oauth/oidc.ts @@ -1,4 +1,5 @@ import { config } from '@/lib/config'; +import { encrypt } from '@/lib/crypto'; import Logger from '@/lib/logger'; import { combine } from '@/lib/middleware/combine'; import { method } from '@/lib/middleware/method'; @@ -7,7 +8,7 @@ import { oidcAuth } from '@/lib/oauth/providerUtil'; import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth'; // thanks to @danejur for this https://github.com/diced/zipline/pull/372 -async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Promise { +async function handler({ code, host }: OAuthQuery, _logger: Logger): Promise { if (!config.features.oauthRegistration) return { error: 'OAuth registration is disabled.', @@ -22,16 +23,19 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom error_code: 401, }; - if (!code) + if (!code) { + const linkState = encrypt('link', config.core.secret); + return { redirect: oidcAuth.url( config.oauth.oidc.clientId!, `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`, config.oauth.oidc.authorizeUrl!, - state, + linkState, config.oauth.oidc.redirectUri ?? undefined, ), }; + } const body = new URLSearchParams({ client_id: config.oauth.oidc.clientId!,