Reputation: 11
I have a Django/DRF API backend that I'm POSTing login credentials to and expecting a "sessionid" cookie in return. It's running at https://url.com/api/. The login endpoint is https://url.com/api/api_login/.
I'm using ExpressJS and fetch on the frontend to make the call. It's running at https://url.com/. The login form is located at https://url.com/login.
I have an Nginx reverse proxy mapping "url.com/api" to "url.com:8002", and "url.com" to "url.com:8003".
Here is the simplified code for the backend:
# 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
Here is the full code for the frontend:
//*** 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);
});
Where I'm at so far:
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'
]
}
}
Here's my interpretation, in case it makes obvious what I'm doing wrong. I'm pretty sure the backend is fine - it's supplying the set-cookie header. So it's a frontend issue and a question why the browser isn't consuming that header and setting the cookie. I do get the "test" cookie that I manually set, so I know it's not because my browser is rejecting cookies. I don't think I have a CORS issue because from both the server and client POV, I'm in the same domain (https://url.com), even though the server and client are going to different ports, cookies should be port-agnostic. But just in case, I did try adding CORS headers for "access-control-allow-origin: https://url.com:8003" but that didn't help either. I'm not getting either the csrf cookie or the sessionid cookie.
Postman also does not get the cookie. Postman can get the cookie if it hits the https://url.com/api/api_login/ endpoint directly.
Upvotes: 1
Views: 1464
Reputation: 11
OK, I've come up with a solution, but perhaps wiser people can tell me if this breaks best practice.
Since DRF is providing the set-cookie headers, I'm using "set-cookie-parser" on the frontend to read the header value and set the cookie manually:
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;
I'm still not entirely sure why my browser wasn't setting the cookie itself. I started to suspect that while my Express server got the response.header with the "set-cookie" directive, it was not passing it along to my browser. Seeing how my test cookie was being set correctly, I decided just to do it explicitly instead. Is this the correct way? Are there security implications? I have no idea. Because I'm also manually setting all the cookie parameters (httpOnly and Secure), I'm assuming it's "just as safe" as if the browser had consume the set-cookie header and done it itself...
Upvotes: 0