For several days now I've been looking for a secure authentication and session management mechanism for my single page application. Judging by the numerous tutorials and blog posts out there about SPAs and authentication, storing JWTs in localStorage or regular cookies seems to be the most common approach, but it's simply not a good idea to use JWTs for sessions so it's not an option for this app.
Requirements
The ideal mechanism seems to be cookie-based authentication using HttpOnly
cookies that contain session IDs. The flow would work like this:
HttpOnly
response cookie.withCredentials
option.Because the cookie has the HttpOnly
flag, JavaScript would not be able to read its contents, so it would be safe from attacks as long as it is transmitted over HTTPS.
Challenges
Here's the specific issue I've run into. My server is configured to handle CORS requests. After authenticating the user, it correctly sends the cookie in the response:
HTTP/1.1 200 OK server: Cowboy date: Wed, 15 Mar 2017 22:35:46 GMT content-length: 59 set-cookie: _myapp_key=SFMyNTYBbQAAABBn; path=/; HttpOnly content-type: application/json; charset=utf-8 cache-control: max-age=0, private, must-revalidate x-request-id: qi2q2rtt7mpi9u9c703tp7idmfg4qs6o access-control-allow-origin: http://localhost:8080 access-control-expose-headers: access-control-allow-credentials: true vary: Origin
However, the browser does not save the cookie (when I check Chrome's local cookies it's not there). So when the following code runs:
context.axios.post(LOGIN_URL, creds).then(response => { context.$router.push("/api/account") }
And the Account page is created:
created() { this.axios.get(SERVER_URL + "/api/account/", {withCredentials: true}).then(response => { //do stuff } }
This call does not have the cookie in the header. The server therefore rejects it.
GET /api/account/ HTTP/1.1 Host: localhost:4000 Connection: keep-alive Accept: application/json Origin: http://localhost:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 Referer: http://localhost:8080/ Accept-Encoding: gzip, deflate, sdch, br Accept-Language: en-US,en;q=0.8,tr;q=0.6
Do I need to do something special to make sure the browser saves the cookie after receiving it in the login response? Various sources I've read have said that browsers save response cookies only when the user is redirected, but I don't want any "hard" redirects since this is an SPA.
The SPA in question is written in Vue.js, but I guess it applies to all SPAs. I'm wondering how people handle this scenario.
Other stuff I've read on this topic:
If a browser does not support HttpOnly and a website attempts to set an HttpOnly cookie, the HttpOnly flag will be ignored by the browser, thus creating a traditional, script accessible cookie. As a result, the cookie (typically your session cookie) becomes vulnerable to theft or modification by malicious script.
What is Cookie-based Authentication? Cookies are pieces of data used to identify the user and their preferences. The browser returns the cookie to the server every time the page is requested. Specific cookies like HTTP cookies are used to perform cookie-based authentication to maintain the session for each user.
Last Updated: September 3, 2021. An HttpOnly Cookie is a tag added to a browser cookie that prevents client-side scripts from accessing data. It provides a gate that prevents the specialized cookie from being accessed by anything other than the server.
I got this working on Vue.js 2 with credentials = true. Setting credentials from client site only half of the story. You need to set response headers from the server as well:
header("Access-Control-Allow-Origin: http://localhost:8080"); header("Access-Control-Allow-Credentials: true");
You can't use wildcard for Access-Control-Allow-Origin like this:
header("Access-Control-Allow-Origin: *");
When you specify the credentials: true header, you are required to specify the orgin.
As you can see, this is a PHP code, you can model it to NodeJS or any server side scripting language you are using.
In VueJS I set credentials = true like this in the main.js:
Vue.http.options.credentials = true
In the component, I successfully logged in using ajax:
<template> <div id="userprofile"> <h2>User profile</h2> {{init()}} </div> </template> <script> export default { name: 'UserProfile', methods: { init: function() { // loggin in console.log('Attempting to login'); this.$http.get('https://localhost/api/login.php') .then(resource => { // logging response - Notice I don't send any user or password for testing purposes }); // check the user profile console.log('Getting user profile'); this.$http.get('https://localhost/api/userprofile.php') .then(resource => { console.log(resource.data); }) } } } </script>
On the server side, things are pretty simple: Login.php on sets a cookie without making any validation whatsoever (Note: this is done for testing purposes only. You are not advised to use this code in production without validation)
<?php header("Access-Control-Allow-Origin: http://localhost:8080"); header("Access-Control-Allow-Credentials: true"); $cookie = setcookie('user', 'student', time()+3600, '/', 'localhost', false , true); if($cookie){ echo "Logged in"; }else{ echo "Can't set a cookie"; }
Finally, the userprofile.php just verifies if a cookie = user is set
<?php header("Access-Control-Allow-Origin: http://localhost:8080"); header("Access-Control-Allow-Credentials: true"); if(isset($_COOKIE['user'])){ echo "Congratulations the user is ". $_COOKIE['user']; }else{ echo "Not allowed to access"; }
Successfully logged in
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With