Браузер не устанавливает 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 и делал это сам...