fix: VK OAuth uses short UUID state + in-memory PKCE store instead of JWT
This commit is contained in:
@@ -3,6 +3,21 @@ import { normalizeEmail } from '../lib/auth.js'
|
|||||||
import { generateAvatar } from '../lib/generate-avatar.js'
|
import { generateAvatar } from '../lib/generate-avatar.js'
|
||||||
import { prisma } from '../lib/prisma.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() {
|
function generatePkcePair() {
|
||||||
const verifier = crypto.randomBytes(48).toString('base64url').slice(0, 64)
|
const verifier = crypto.randomBytes(48).toString('base64url').slice(0, 64)
|
||||||
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
|
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 redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
|
||||||
const { codeVerifier, codeChallenge } = generatePkcePair()
|
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')
|
const url = new URL('https://id.vk.ru/authorize')
|
||||||
url.searchParams.set('client_id', clientId)
|
url.searchParams.set('client_id', clientId)
|
||||||
@@ -125,10 +141,8 @@ export async function registerOAuthSocialRoutes(fastify) {
|
|||||||
|
|
||||||
const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
|
const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
|
||||||
const { codeVerifier, codeChallenge } = generatePkcePair()
|
const { codeVerifier, codeChallenge } = generatePkcePair()
|
||||||
const state = fastify.jwt.sign(
|
const state = crypto.randomUUID()
|
||||||
{ oauth: 'vk', action: 'link', userId: request.user.sub, cv: codeVerifier },
|
storePkce(state, codeVerifier, { action: 'link', userId: request.user.sub })
|
||||||
{ expiresIn: '15m' },
|
|
||||||
)
|
|
||||||
|
|
||||||
const url = new URL('https://id.vk.ru/authorize')
|
const url = new URL('https://id.vk.ru/authorize')
|
||||||
url.searchParams.set('client_id', clientId)
|
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'))
|
return oauthErrorRedirect(reply, String(query.error_description || query.error || 'ошибка VK'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const statePayload = (() => {
|
const state = typeof query.state === 'string' ? query.state.trim() : ''
|
||||||
try {
|
if (!state) return oauthErrorRedirect(reply, 'Недействительный state OAuth')
|
||||||
const raw = typeof query.state === 'string' ? query.state : ''
|
|
||||||
return fastify.jwt.verify(raw || '')
|
const pkceEntry = consumePkce(state)
|
||||||
} catch {
|
if (!pkceEntry) return oauthErrorRedirect(reply, 'Недействительный state OAuth')
|
||||||
return null
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
if (!statePayload) return oauthErrorRedirect(reply, 'Недействительный state OAuth')
|
|
||||||
|
|
||||||
const code = typeof query.code === 'string' ? query.code.trim() : ''
|
const code = typeof query.code === 'string' ? query.code.trim() : ''
|
||||||
if (!code) return oauthErrorRedirect(reply, 'Не получен код от VK')
|
if (!code) return oauthErrorRedirect(reply, 'Не получен код от VK')
|
||||||
@@ -166,14 +176,13 @@ export async function registerOAuthSocialRoutes(fastify) {
|
|||||||
const clientId = process.env.VK_CLIENT_ID
|
const clientId = process.env.VK_CLIENT_ID
|
||||||
const clientSecret = process.env.VK_CLIENT_SECRET
|
const clientSecret = process.env.VK_CLIENT_SECRET
|
||||||
const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
|
const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
|
||||||
const codeVerifier = typeof statePayload.cv === 'string' ? statePayload.cv : ''
|
|
||||||
|
|
||||||
const body = new URLSearchParams()
|
const body = new URLSearchParams()
|
||||||
body.set('grant_type', 'authorization_code')
|
body.set('grant_type', 'authorization_code')
|
||||||
body.set('client_id', clientId)
|
body.set('client_id', clientId)
|
||||||
body.set('client_secret', clientSecret)
|
body.set('client_secret', clientSecret)
|
||||||
body.set('code', code)
|
body.set('code', code)
|
||||||
body.set('code_verifier', codeVerifier)
|
body.set('code_verifier', pkceEntry.codeVerifier)
|
||||||
body.set('redirect_uri', redirectUri)
|
body.set('redirect_uri', redirectUri)
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
body.set('device_id', deviceId)
|
body.set('device_id', deviceId)
|
||||||
@@ -199,7 +208,7 @@ export async function registerOAuthSocialRoutes(fastify) {
|
|||||||
if (!vkUserId) return oauthErrorRedirect(reply, 'no_user_id')
|
if (!vkUserId) return oauthErrorRedirect(reply, 'no_user_id')
|
||||||
if (!emailSuggestion) return oauthErrorRedirect(reply, 'no_email')
|
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({
|
const user = await findOrCreateUserFromOAuth({
|
||||||
provider: 'vk',
|
provider: 'vk',
|
||||||
|
|||||||
Reference in New Issue
Block a user