Аутентификация полного стека: файлы cookie и локальное хранилище — Блог о самом интересном.

От автора: когда вы авторизуетесь в приложении, вы ожидаете, что в следующий раз, когда вы откроете новую вкладку или окно в браузере, вы все равно останетесь авторизованным. Это означает, что в некотором роде или форме, клиент (браузер) должен поддерживать ссылку на вас, чтобы вы оставались авторизованным.

Где я могу сохранить в клиенте состояние?

Работа с безопасностью и аутентификацией в приложении фронт-энд может быть сложной проблемой. Существует всего два способа поддерживать состояние клиента в веб-приложении:

Локальное хранилище

Cookies

Уязвимости?

Оба эти метода связаны с потенциальными проблемами безопасности:

Локальное хранилище: XSS — межсайтовый скриптинг. Уязвимость XSS позволяет злоумышленнику внедрить JavaScript на сайт.

Cookies: CSRF — подделка межсайтовых запросов. CSRF уязвимость позволяет злоумышленнику выполнять действия на веб-сайте от имени авторизованного пользователя.

Как я могу защититься от этого?

Если локальное хранилище может быть использовано сторонними скриптами (например, найденными в расширениях браузера) и если аутентификация может быть подделана с помощью файлов cookie, где допустимо размещать состояние клиента?

Из раздела Аутентификация в одностраничных приложениях с помощью файлов Cookies документации Auth0 мы узнаем, что если ваше приложение:

предоставляется клиенту с использованием собственного бэк-энда

имеет тот же домен, что и ваш бэк-энд

выполняет вызовы API, которые требуют аутентификации для бэк-энда

тогда есть способ безопасно использовать для аутентификации cookie.

На что это похоже?

Реальный пример установки:

Одностраничное приложение (SPA) React на фронт-энд

Серверная часть Node + Express

Web Cookies (Secure, HttpOnly, Same Site)

Сервер Express будет обслуживать React SPA по всем маршрутам, кроме тех, которые начинаются с /api. Приложение React использует сервер Express для всех конечных точек. Благодаря этому методу фронт-энд приложение находится в том же домене и имеет сервер, позволяющий защищать файлы cookie с помощью параметров HttpOnly, Secure и Same Site.

Отсюда вы можете выполнять вызовы API для микросервисов или некоторых защищенных серверов. Конечные точки API и токены доступа не будут видны из браузера. Ниже я изложу некоторые основные концепции настройки этой архитектуры для приложения полного стека.

Использование HTTP-файлов cookie в Express

Для того, чтобы использовать cookie в Express, вы используете модуль cookie-parser.

Парсинг cookie

JavaScript const cookieParser = require(‘cookie-parser’)

app.use(cookieParser())

123 const cookieParser = require(‘cookie-parser’) app.use(cookieParser())

Установка cookie

Вы можете установить cookie маршрута для объекта response, указав некоторые важные свойства:

JavaScript // Set a cookie response.cookie(‘nameOfCookie’, ‘cookieValue’, { maxAge: 60 * 60 * 1000, // 1 hour httpOnly: true, secure: true, sameSite: true,

})

Видео VK пользуются все большей популярностью среди пользователей

1234567 // Set a cookieresponse.cookie(‘nameOfCookie’, ‘cookieValue’, {  maxAge: 60 * 60 * 1000, // 1 hour  httpOnly: true,  secure: true,  sameSite: true,})

Same Site — предотвращает отправку cookie в межсайтовых запросах

HTTP Only — файлы cookie доступны только с сервера

Secure — cookie должны передаваться по HTTPS

Получение cookie

Файл cookie теперь можно прочитать в последующих ответах.

JavaScript // Get a cookie

response.cookies.nameOfCookie

12 // Get a cookieresponse.cookies.nameOfCookie

Очистка cookie

После выхода из системы, вам нужно удалить cookie.

JavaScript // Clear a cookie

response.clearCookie(‘nameOfCookie’)

12 // Clear a cookieresponse.clearCookie(‘nameOfCookie’)

Локальные значения в middleware Express

Express работает на middleware. Если вы хотите обновить cookie в одном middleware и использовать его в следующем, вы можете сохранить его как локальный Express. Это может пригодиться, если вам нужно обновить токен доступа JWT по маршруту preAuth, использовать эту аутентификацию в обработчике и отправить в конце cookie в ответе.

JavaScript // Create a local const refreshMiddleware = (request, response, next) => { const accessToken = getNewAccessToken(refreshToken) // Set local response.locals.accessToken = accessToken next()

}

// Use a local const handler = (request, response) => { const updatedAccessToken = response.locals.accessToken

}

router.post(‘/app/user’, refreshMiddleware, handler)

1234567891011121314 // Create a localconst refreshMiddleware = (request, response, next) => {  const accessToken = getNewAccessToken(refreshToken)  // Set local  response.locals.accessToken = accessToken  next()} // Use a localconst handler = (request, response) => {  const updatedAccessToken = response.locals.accessToken} router.post(‘/app/user’, refreshMiddleware, handler)

Обслуживание фронт-энд приложения

Хороший пример этой настройки можно найти в настройке шаблона Simple React Full Stack. В конечном итоге вот как будет выглядеть макет вашего приложения:

JavaScript dist # Distribution folder of the production React SPA build src client # React source files

server # Express server files

1234 dist     # Distribution folder of the production React SPA buildsrc  client # React source files  server # Express server files

В этом случае файл сервера будет выглядеть примерно так:

JavaScript // src/server/index.js

// Initialize Express app const express = require(‘express’) const app = express()

const router = require(‘./router’)

// Serve all static files from the dist folder
app.use(express.static(path.join(__dirname, ‘../../dist/’)))

// Set up express router to serve all api routes (more on this below)
app.use(‘/api’, router)

// Serve any other file as the distribution index.html app.get(‘*’, (request, response) => { response.sendFile(path.join(__dirname, ‘../../dist/index.html’))

})

1234567891011121314151617 // src/server/index.js // Initialize Express appconst express = require(‘express’)const app = express()const router = require(‘./router’) // Serve all static files from the dist folderapp.use(express.static(path.join(__dirname, ‘../../dist/’))) // Set up express router to serve all api routes (more on this below)app.use(‘/api’, router) // Serve any other file as the distribution index.htmlapp.get(‘*’, (request, response) => {  response.sendFile(path.join(__dirname, ‘../../dist/index.html’))})

Маршруты и обработчики Express

Используя класс Express Router, вы можете организовать все маршруты API в подкаталоги и объединить их одной строкой в главной точке входа сервера.

Три CSS альтернативы навигации в JavaScript

JavaScript src server router handlers

index.js

12345 src  server    router    handlers    index.js

Все маршруты могут быть организованы в отдельные подкаталоги.

JavaScript // src/server/routes/index.js

const router = require(‘express’).Router() const bookRoutes = require(‘./books’)

const authorRoutes = require(‘./authors’)

router.use(‘/books’, bookRoutes)
router.use(‘/authors’, authorRoutes)

module.exports = router

12345678910 // src/server/routes/index.js const router = require(‘express’).Router()const bookRoutes = require(‘./books’)const authorRoutes = require(‘./authors’) router.use(‘/books’, bookRoutes)router.use(‘/authors’, authorRoutes) module.exports = router

В одном наборе маршрутов, мы можем определить все маршруты GET, POST, DELETE и т.д. Поскольку маршрутизатор использует /api, и маршрут авторов использует /authors, вызов GET API для /api/authors/jk-rowling вызовет в этом примере обработчик getAuthor.

JavaScript // src/server/routes/authors.js

const router = require(‘express’).Router()
const authorHandlers = require(‘../handlers/authors’)

// Get router.get(‘/’, authorHandlers.getAllAuthors)

router.get(‘/:author’, authorHandlers.getAuthor)

// Post
router.post(‘/’, authorHandlers.addAuthor)

module.exports = router

12345678910111213 // src/server/routes/authors.js const router = require(‘express’).Router()const authorHandlers = require(‘../handlers/authors’) // Getrouter.get(‘/’, authorHandlers.getAllAuthors)router.get(‘/:author’, authorHandlers.getAuthor) // Postrouter.post(‘/’, authorHandlers.addAuthor) module.exports = router

Вы можете поместить все связанные с ними обработчики автора в подкаталог handlers.

JavaScript // src/server/handlers/authors.js

module.exports = { getAllAuthors: async (request, response) => { // Some logic… if (success) { response.status(200).send(authors) } else { response.status(400).send({ message: ‘Something went wrong’ }) } }, addAuthor: async (request, response) => { … },

}

12345678910111213 // src/server/handlers/authors.js module.exports = {  getAllAuthors: async (request, response) => {    // Some logic…    if (success) {      response.status(200).send(authors)    } else {      response.status(400).send({ message: ‘Something went wrong’ })    }  },  addAuthor: async (request, response) => { … },}

Это возвращает нас к точке входа на сервер, которая вводит все маршруты для /api.

JavaScript // src/server/index.js

// Set up all API routes
const router = require(‘./router’)

// Use all API routes
app.use(‘/api’, router)

1234567 // src/server/index.js // Set up all API routesconst router = require(‘./router’) // Use all API routesapp.use(‘/api’, router)

Одностраничное приложение React

У Тайлера МакГинниса (Tyler McGinnis) есть отличная статья о защищенных маршрутах и аутентификации с помощью React Router, в которой рассказано, как можно создать компоненты PrivateRoute и PublicRoute.

Это защита аутентификации только на уровне фронт-энд. Ей нельзя доверять защиту конфиденциальных данных, которые должны быть защищены внутренними API, требующими маркеры доступа (или любого другого метода защиты) для возврата ответа.

Используя базовый пример маршрутов из упомянутой выше статьи, ниже показано, как вы можете выполнить API-вызов сервера Express из React, аутентифицировать некоторое состояние глобального контекста и направить приложение через фронт-энд.

JavaScript // App.js

import React, { Component } from ‘react’ import { BrowserRouter as Router, Switch, Route, Redirect } from ‘react-router-dom’ import axios from ‘axios’

// …plus page and context imports

export default class App extends Component {
static contextType = AuthContext

state = { loading: true }

async componentDidMount() {
const Auth = this.context

try {
const response = await axios(‘/api/auth’)

Auth.authenticate() } catch (error) { console.log(error) } finally { this.setState({ loading: false }) }

}

render() { const Auth = this.context

const { loading } = this.state

if (loading) { return Loading…

}

return ( ) }

}

12345678910111213141516171819202122232425262728293031323334353637383940414243444546 // App.js import React, { Component } from ‘react’import { BrowserRouter as Router, Switch, Route, Redirect } from ‘react-router-dom’import axios from ‘axios’// …plus page and context imports export default class App extends Component {  static contextType = AuthContext   state = { loading: true }   async componentDidMount() {    const Auth = this.context     try {      const response = await axios(‘/api/auth’)       Auth.authenticate()    } catch (error) {      console.log(error)    } finally {      this.setState({ loading: false })    }  }   render() {    const Auth = this.context    const { loading } = this.state     if (loading) {      return Loading…    }     return (                                                                        )  }}

Теперь сервер разработки направит вас к правильному маршруту в зависимости от вашего статуса аутентификации. В производственном режиме будет обслуживаться файл index.html — подробнее об этом ниже.

Производство и разработка

При производственной настройке создается все приложение React для распространения, а приложение Express обслуживает SPA на всех маршрутах.

JavaScript // package.json

// Production { «build»: «cross-env NODE_ENV=production webpack —config config/webpack.prod.js», «start»: «npm run build && node src/server/index.js»

}

1234567 // package.json // Production{  «build»: «cross-env NODE_ENV=production webpack —config config/webpack.prod.js»,  «start»: «npm run build && node src/server/index.js»}

Для разработки это громоздко. Лучший способ для разработки — это обслуживать React на сервере разработки Webpack, как вы это делаете регулярно, и передавать все запросы API на сервер Express.

JavaScript // package.json

// Development { «client»: «cross-env NODE_ENV=development webpack-dev-server —config config/webpack.dev.js», «server»: «nodemon src/server/index.js», «dev»: «concurrently «npm run server» «npm run client»»

}

12345678 // package.json // Development{  «client»: «cross-env NODE_ENV=development webpack-dev-server —config config/webpack.dev.js»,  «server»: «nodemon src/server/index.js»,  «dev»: «concurrently «npm run server» «npm run client»»}

Вы, вероятно, будете обслуживать приложение React через порт 3000, а сервер через порт 5000, которые можно установить в конфигурационном файле Webpack для разработки.

JavaScript devServer: { historyApiFallback: true, proxy: { ‘/api’: ‘http://localhost:5000’, }, open: true, compress: true, hot: true, port: 3000,

}

12345678910 devServer: {  historyApiFallback: true,  proxy: {    ‘/api’: ‘http://localhost:5000’,  },  open: true,  compress: true,  hot: true,  port: 3000,}

Настройка historyApiFallback обеспечит правильную работу маршрутов приложения. Также важно установить для publicPathв Webpack значение /, чтобы гарантировать, что производственные маршруты обслуживают пакеты из корня.

Webpack Boilerplate является хорошим примером того, как настроить Webpack (в этом случае, вы бы просто переместили все из директории сборки для src непосредственно в src/client).

Заключение

Надеемся, что этот ресурс помог вам понять различные типы уязвимостей, связанных с постоянным хранилищем на стороне клиента (XSS и CSRF), а также некоторые подходы, которые мы можем использовать для защиты от атак, а именно HttpOnly, SameSite, Secure Web Cookies.

Автор: Tania Rascia

Редакция: Команда webformyself.

Вам также может понравиться