Как реализовать надежное и безопасное управление сеансами (в целом и в Next.js)

Я использую Next.js и хочу правильно реализовать управление сеансами в этой бессерверной архитектуре. Я проверил nextauth, но на самом деле я не фанат, и он не реализует некоторые вещи, которые, как мне кажется, могут мне понадобиться, поэтому просто пытаюсь упростить и делать это сам. Я также прочитал экспресс-сессию, откуда исходит большая часть этого кода.

Вот основная часть утилит управления сеансом:

import stringifyJSON from './stringify-json'
import moment from 'moment'
import knex from 'initializers/knex'
const crypto = require('crypto')
const cookie = require('cookie')
const signature = require('cookie-signature')

const random = (size) => {
  return new Promise((res, rej) => {
    crypto.randomBytes(size, function(err, rnd) {
      if (err) return rej(err)
      res(rnd)
    })
  })
}

export const generateSessionId = async () => {
  const sessionIdBuffer = await random(32)
  const sessionId = sessionIdBuffer.toString('hex')
  return sessionId
}

export function getcookie(cookieHeader, name, secret) {
  var raw;
  var val;

  var cookies = cookie.parse(cookieHeader);

  raw = cookies[name];

  if (raw) {
    if (raw.substr(0, 2) === 's:') {
      val = signature.unsign(raw.slice(2), secret)

      if (val === false) {
        val = undefined;
      }
    } else {
    }
  }

  return val
}

export function setcookie(res, name, val, secret, options) {
  var signed = 's:' + signature.sign(val, secret);
  var data = cookie.serialize(name, signed, options);

  var prev = res.getHeader('Set-Cookie') || []
  var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];

  res.setHeader('Set-Cookie', header)
}

export async function resolvePublicSession(req, res) {
  const privateSession = await resolvePrivateSession(req, res)
  const user = await knex.from('user').select('*').where('id', privateSession.user_id).first()

  if (!user.is_guest) {
    const avatar = await knex.from('image')
      .where('id', user.avatar_id)
      .select('*')
      .first()

    if (avatar) {
      avatar.sources = await knex.from('image_source')
        .where('image_id', avatar.id)
        .returning('*')
        .all()

      user.avatar = avatar
    }
  }

  const session = {
    user,
    ip: req.headers['x-real-ip'] || req.connection.remoteAddress,
    // csrfToken
  }

  return JSON.parse(stringifyJSON(session))
}

export async function resolvePrivateSession(req, res) {
  let session

  if (req.headers.cookie) {
    // TODO: move these secrets/names into config somewhere
    const token = getcookie(req.headers.cookie, 'head', 'a secret')

    if (token) {
      session = await knex('session')
        .select('*')
        .where('token', token)
        .first()

      if (session) {
        if (session.expires_at <= new Date) {
          session.token = await generateSessionId()
          session.expires_at = moment().add('days', 30).toDate()
          await knex('session').where('token', token)
            .update({
              token: session.token,
              expires_at: session.expires_at
            })
        }
      }
    }
  }

  if (!session) {
    const [guestUser] = await knex('user').insert({ is_guest: true })
    const newToken = await generateSessionId()
    const expires_at = moment().add('days', 30).toDate()
    const sessions = await knex('session')
      .returning('*')
      .insert({
        user_id: guestUser.id,
        token: newToken,
        expires_at
      })
    session = sessions[0]
  }

  setcookie(res, 'head', session.token, 'a secret', {
    // maxAge:
    // domain:
    path: "https://codereview.stackexchange.com/",
    expires: session.expires_at,
    httpOnly: process.env.NODE_ENV === 'production',
    secure: process.env.NODE_ENV === 'production',
    sameSite: process.env.NODE_ENV === 'production',
  })

  return session
}

И вот как его можно использовать в вызове серверного API:

export default async function(req, res) {
  const { code } = req.query
  try {
    const session = await sessionController.resolve(req, res)
    const creds = await linkedinController.getAccessToken(code)
    await linkedinController.upsertAccount(session, creds)
    const user = await userController.getFromSession(session)
    if (user.is_guest) {
      await userController.promote(user)
    }
    res.redirect(`/name`)
  } catch (e) {

  }
}

И вот как это можно использовать во внешнем интерфейсе:

export default function MyPage({ session }) {
  // handle session object, and render view
}

export async function getServerSideProps({ req, res }) {
  const sessionController = require('../utils/session')
  const session = await sessionController.resolvePublicSession(req, res)

  return {
    props: { session },
  }
}

Что не так и / или чего не хватает в том, чтобы сделать это крепкий система управления сеансами? Я думаю, что мне все еще нужно добавить токены csrf где-нибудь в миксе (пока не знаю, где, возможно, вы можете посоветовать по этому поводу). Но кроме этого, я думаю, это все, что вам действительно нужно. Что еще мне нужно и / или что я делаю не так?

Основная часть кода чтения / записи файлов cookie поступает прямо из экспресс-сессия (слегка изменен, чтобы разрешить только один секрет, не знаю, почему они использовали массив секретов).

В основном, как это работает, внутри обработчика запросов API Next.js вы вызываете sessionController.resolvePrivateSession, что приводит к созданию объекта сеанса прямо из таблицы сеансов в базе данных, которая имеет token (уникальные случайные байты в шестнадцатеричном формате) и время expires_at, которое в настоящее время я не знаю, как это использовать, как мне использовать / реализовать функцию expires_at?

Процесс «возврата объекта сеанса из базы данных» действительно довольно сложен. Сначала он пытается проанализировать файл cookie, который может выглядеть так:

s:fd51d213b6935caf5d987dac8dbadf3674dba89dac5d976ab56b5ba88cdf731b.O9jinFhrd7F/R+oazU3+5pxzAaBwbybt+meJTjcQKLg

Замечая s:, он знает, как проверить токен. Он использует стандартную библиотеку node.js для отмены подписи файла cookie, который возвращает false или исходный токен. Я не совсем уверен, как это работает внутри, но полагаю, что это правильно. Есть чем заняться там.

Затем он пытается найти сеанс из БД, используя этот token это получилось из печенья. Если он его находит, он проверяет, истек ли срок сеанса, и в этом случае я не уверен, должен ли я просто сгенерировать новый токен или что-то в этом роде. О, у меня здесь тоже есть “гостевая” система, так что есть всегда пользователь (так что вещи можно отслеживать более систематично). Я думаю, что это может быть спамлено и создать кучу пользователей, поэтому периодически сеансы с гостевыми пользователями должны просто очищаться из базы данных, но это касательно. Но если он нашел сеанс, теперь мы можем найти user_id хранится в объекте session db, и идем оттуда и делаем то, что нам нужно. В этом вся цель этой функции.

Но затем, непосредственно перед возвратом session + user_id, он записывает подписанный файл cookie в заголовок ответа. Я не думаю, что что-то еще нужно передавать кодировщику в виде строки, но экспресс-сеанс передается в хеше всего JSON.stringify (sessionObject), что кажется большим и ненужным, поэтому я вырезал его. Но затем у нас есть записанный файл cookie, в котором скрыт подписанный токен, поэтому мы можем получить его в следующем запросе. Это все, что нужно для управления сеансом? Что мне не хватает?

Ключевые вопросы:

  • Что вы делаете с полем expires_at в объекте сеанса db?
  • Нужно ли что-то еще добавить, чтобы сделать это более безопасным?

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *