diff --git a/server/src/routes/oauth-social.js b/server/src/routes/oauth-social.js index 9c09a34..5e6137e 100644 --- a/server/src/routes/oauth-social.js +++ b/server/src/routes/oauth-social.js @@ -3,6 +3,21 @@ import { normalizeEmail } from '../lib/auth.js' import { generateAvatar } from '../lib/generate-avatar.js' import { prisma } from '../lib/prisma.js' +const pkceStore = new Map() + +function storePkce(state, codeVerifier, meta = {}) { + pkceStore.set(state, { codeVerifier, meta, createdAt: Date.now() }) +} + +function consumePkce(state) { + const entry = pkceStore.get(state) + if (entry) { + pkceStore.delete(state) + return { codeVerifier: entry.codeVerifier, meta: entry.meta } + } + return null +} + function generatePkcePair() { const verifier = crypto.randomBytes(48).toString('base64url').slice(0, 64) const challenge = crypto.createHash('sha256').update(verifier).digest('base64url') @@ -99,7 +114,8 @@ export async function registerOAuthSocialRoutes(fastify) { const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback` const { codeVerifier, codeChallenge } = generatePkcePair() - const state = fastify.jwt.sign({ oauth: 'vk', cv: codeVerifier }, { expiresIn: '15m' }) + const state = crypto.randomUUID() + storePkce(state, codeVerifier) const url = new URL('https://id.vk.ru/authorize') url.searchParams.set('client_id', clientId) @@ -125,10 +141,8 @@ export async function registerOAuthSocialRoutes(fastify) { const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback` const { codeVerifier, codeChallenge } = generatePkcePair() - const state = fastify.jwt.sign( - { oauth: 'vk', action: 'link', userId: request.user.sub, cv: codeVerifier }, - { expiresIn: '15m' }, - ) + const state = crypto.randomUUID() + storePkce(state, codeVerifier, { action: 'link', userId: request.user.sub }) const url = new URL('https://id.vk.ru/authorize') url.searchParams.set('client_id', clientId) @@ -148,15 +162,11 @@ export async function registerOAuthSocialRoutes(fastify) { return oauthErrorRedirect(reply, String(query.error_description || query.error || 'ошибка VK')) } - const statePayload = (() => { - try { - const raw = typeof query.state === 'string' ? query.state : '' - return fastify.jwt.verify(raw || '') - } catch { - return null - } - })() - if (!statePayload) return oauthErrorRedirect(reply, 'Недействительный state OAuth') + const state = typeof query.state === 'string' ? query.state.trim() : '' + if (!state) return oauthErrorRedirect(reply, 'Недействительный state OAuth') + + const pkceEntry = consumePkce(state) + if (!pkceEntry) return oauthErrorRedirect(reply, 'Недействительный state OAuth') const code = typeof query.code === 'string' ? query.code.trim() : '' if (!code) return oauthErrorRedirect(reply, 'Не получен код от VK') @@ -166,14 +176,13 @@ export async function registerOAuthSocialRoutes(fastify) { const clientId = process.env.VK_CLIENT_ID const clientSecret = process.env.VK_CLIENT_SECRET const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback` - const codeVerifier = typeof statePayload.cv === 'string' ? statePayload.cv : '' const body = new URLSearchParams() body.set('grant_type', 'authorization_code') body.set('client_id', clientId) body.set('client_secret', clientSecret) body.set('code', code) - body.set('code_verifier', codeVerifier) + body.set('code_verifier', pkceEntry.codeVerifier) body.set('redirect_uri', redirectUri) if (deviceId) { body.set('device_id', deviceId) @@ -199,7 +208,7 @@ export async function registerOAuthSocialRoutes(fastify) { if (!vkUserId) return oauthErrorRedirect(reply, 'no_user_id') if (!emailSuggestion) return oauthErrorRedirect(reply, 'no_email') - const linkToUserId = statePayload?.action === 'link' ? statePayload.userId : undefined + const linkToUserId = pkceEntry.meta?.action === 'link' ? pkceEntry.meta.userId : undefined const user = await findOrCreateUserFromOAuth({ provider: 'vk',