Браузер не устанавливает cookie (set-cookie находится в заголовке) - Django backend, Express fetch frontend

Прошу прощения, если этот вопрос уже задавался, но я прочесал Google за 5 дней и просто застрял.

У меня есть бэкенд Django/DRF API, в который я отправляю учетные данные для входа и ожидаю в ответ куки "sessionid". Он работает по адресу https://url.com/api/. Конечная точка входа - https://url.com/api/api_login/.

Для выполнения вызова я использую ExpressJS и fetch на фронтенде. Он запущен по адресу https://url.com/. Форма входа расположена по адресу https://url.com/login.

У меня есть обратный прокси Nginx, отображающий "url.com/api" на "url.com:8002", а "url.com" на "url.com:8003".

Вот упрощенный код для бэкенда:

# views.py

@method_decorator(csrf_exempt, name='dispatch')
class ApiLogin(APIView):
  def post(self, request):
    form = LoginForm(request.POST)
    if form.is_valid():
      user = authenticate(request, username=form.cleaned_data['username'], password=form.cleaned_data['password'])
      if user is not None:
        auth_login(request, user)

    # at this point, you are either logged in or not
    if request.user.is_authenticated:
      response = HttpResponse(f"Successful login for {form.cleaned_data['username']}.")
      return response
    else:
      response = HttpResponse("Login failed.")
      return response

Вот полный код для фронтенда:

//*** server.js
const express = require('express');
const app = express();
app.use(express.static('static'));
app.use(express.json());
app.use(express.urlencoded({extended: true}));

const router = require('./router');
app.use(router);



//*** router.js
const express = require('express');
const router = express.Router();
const qs = require('qs');
const fetch = require('node-fetch');

// temporarily running on a self-signed cert, so this bypasses the cert-check
const https = require('https');
const httpsAgent = new https.Agent({
  rejectUnauthorized: false
});

router.get('/login', (req, res) => {
  res.render('login');
});

router.post('/login', (req, res) => {
  fetch('https://url.com/api/api_login/', {
    method: 'POST',
    body: qs.stringify({
      'username': req.body.username,
      'password': req.body.password
    }),
    agent: httpsAgent,
    credentials: 'include',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    }
  })
  .then(response => {
    console.log(response.headers);
    res.cookie("test", "value");
    res.render('home');
  })
  .catch(error => {
    console.error(error);
  });

Где я нахожусь на данный момент:

  • Я возился с CORS как на стороне сервера Django, так и на стороне клиента Express (всевозможные комбинации access-control-allow-origin и access-control-allow-credentials). В моей текущей итерации кода, которую я разместил здесь, это вычеркнуто.
  • Я возился с настройками cookie (httpOnly true/false, secure true/false, SameSite Lax/None, path=/, expires in the future)
  • .
  • Я попробовал axios вместо fetch() и использование withCredentials: true
  • .
  • Моя текущая итерация - fetch() с полномочиями: 'include'
  • Экспресс console.log(response.headers) действительно ДОЛЖЕН показывать строки "set-cookie":
Headers {
  [Symbol(map)]: [Object: null prototype] {
    server: [ 'nginx/1.21.6' ],
    date: [ 'Fri, 08 Apr 2022 04:48:49 GMT' ],
    'content-type': [ 'text/html; charset=utf-8' ],
    'content-length': [ '29' ],
    connection: [ 'close' ],
    vary: [ 'Accept, Cookie' ],
    allow: [ 'POST, OPTIONS' ],
    'x-frame-options': [ 'DENY' ],
    'x-content-type-options': [ 'nosniff' ],
    'referrer-policy': [ 'same-origin' ],
    'set-cookie': [
      'csrftoken=vNgTkUBruc1xeL27KvBYi9esw12hxK8ohQHWQlur7lmiErddU9FVXRnG0Dxas3v2; expires=Fri, 07 Apr 2023 04:48:49 GMT; Max-Age=31449600; Path=/; SameSite=Lax',
      'sessionid=.eJxVjMsOwiAUBf-FtSEgj1KX7v0Gch8gVUOT0q6M_y5NutDtzJzzFhG2tcStpSVOLC5Ci9MvQ6BnqrvgB9T7LGmu6zKh3BN52CZvM6fX9Wj_Dgq00tcjDAGCs8Yo6w0pjQwqe8g2qY6QOeRw1oq7oqyRPA_OOI0q0AjJiM8X2AE34Q:1ncgYD:vRmuQlX4P82-Utw8qmPzSoS-t6Xo7D89CO0UBtyltVY; expires=Fri, 22 Apr 2022 04:48:49 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax'
    ]
  }
}

Вот моя интерпретация, если вдруг станет ясно, что я делаю не так. Я уверен, что бэкенд в порядке - он предоставляет заголовок set-cookie. Так что это проблема фронтенда и вопрос, почему браузер не потребляет этот заголовок и не устанавливает куки. Я получаю "тестовый" куки, который я установил вручную, поэтому я знаю, что это не потому, что мой браузер отвергает куки. Я не думаю, что у меня проблема CORS, потому что с точки зрения сервера и клиента я нахожусь в одном домене (https://url.com), даже если сервер и клиент обращаются к разным портам, куки должны быть независимы от порта. Но на всякий случай я попробовал добавить CORS-заголовки для "access-control-allow-origin: https://url.com:8003", но и это не помогло. Я не получаю ни куки csrf, ни куки sessionid.

Postman также не получает cookie. Postman может получить cookie, если обратится непосредственно к конечной точке https://url.com/api/api_login/.

Я буду рад предоставить еще больше деталей! Мой проект остановился почти на неделю!

OK, я придумал решение, но, возможно, более мудрые люди подскажут мне, нарушает ли это лучшую практику.

Поскольку DRF предоставляет заголовки set-cookie, я использую "set-cookie-parser" на фронтенде для чтения значения заголовка и установки cookie вручную:

const express = require('express');
const router = express.Router();
const qs = require('qs');
const fetch = require('node-fetch');
var setCookie = require('set-cookie-parser');

const api_url = "";

const https = require('https');
const httpsAgent = new https.Agent({
  rejectUnauthorized: false
});

router.get('/login', (req, res) => {
  res.render('login');
});

router.post('/login', (req, res) => {
  fetch(api_url + '/api_login/', {
    method: 'POST',
    body: qs.stringify({
      'username': req.body.username,
      'password': req.body.password
    }),
    agent: httpsAgent,
    credentials: 'include',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  })
  .then(response => { 
    const cookies = setCookie.parse(response.headers.raw()['set-cookie'], {
      decodeValues: true,
    });
    cookies.forEach(cookie => {
      res.cookie(cookie['name'], cookie['value'], {
        expires: cookie['expires'],
        httpOnly: cookie['httpOnly'],
        maxAge: cookie['maxAge'],
        path: cookie['path'],
        sameSite: cookie['sameSite'],
        secure: cookie['secure'],
      })
    });
    return response.text();
  })
  .then(text => {
    console.log(text);
    res.render('home');
  })
  .catch(error => {
    console.error(error);
  });
});

module.exports = router;

Я все еще не совсем уверен, почему мой браузер не устанавливал cookie сам. Я начал подозревать, что хотя мой Express-сервер получает заголовок response.header с директивой "set-cookie", он не передает его моему браузеру. Видя, что мой тестовый cookie устанавливается правильно, я решил просто сделать это явно. Является ли это правильным способом? Есть ли последствия для безопасности? Понятия не имею. Поскольку я также вручную устанавливаю все параметры cookie (httpOnly и Secure), я предполагаю, что это "так же безопасно", как если бы браузер потреблял заголовок set-cookie и делал это сам...

Вернуться на верх