Problem
I have a REST API that has a login endpoint. The login endpoint accepts a username and password, the server responds by sending a HTTPOnly Cookie containing some payload (like JWT).
The approach I always use had been working for a few years until the Set-Cookie header stopped working roughly last week. I have not touched the REST API's source prior to its non-functionality, as I was working on a Svelte-based front-end.
I suspect it has something to do with the Secure attribute being set to false as it is in localhost. However, according to Using HTTP cookies, having an insecure connection should be fine as long as it's localhost. I've been developing REST APIs in this manner for some time now and was surprised to see the cookie no longer being set.
Testing the API with Postman yields the expected result of having the cookie set.
Approaches Used
I tried to recreate the general flow of the real API and stripped it down to its core essentials.
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/golang-jwt/jwt/v4"
)
const idleTimeout = 5 * time.Second
func main() {
app := fiber.New(fiber.Config{
IdleTimeout: idleTimeout,
})
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept, Range",
AllowCredentials: true,
AllowMethods: "GET,POST,HEAD,DELETE,PUT",
ExposeHeaders: "X-Total-Count, Content-Range",
}))
app.Get("/", hello)
app.Post("/login", login)
go func() {
if err := app.Listen("0.0.0.0:8080"); err != nil {
log.Panic(err)
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
_ = <-c
fmt.Println("\n\nShutting down server...")
_ = app.Shutdown()
}
func hello(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
}
func login(c *fiber.Ctx) error {
type LoginInput struct {
Email string `json:"email"`
}
var input LoginInput
if err := c.BodyParser(&input); err != nil {
return c.Status(400).SendString(err.Error())
}
stringUrl := fmt.Sprintf("https://jsonplaceholder.typicode.com/users?email=%s", input.Email)
resp, err := http.Get(stringUrl)
if err != nil {
return c.Status(500).SendString(err.Error())
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return c.Status(500).SendString(err.Error())
}
if len(body) > 0 {
fmt.Println(string(body))
} else {
return c.Status(400).JSON(fiber.Map{
"message": "Yeah, we couldn't find that user",
})
}
token := jwt.New(jwt.SigningMethodHS256)
cookie := new(fiber.Cookie)
claims := token.Claims.(jwt.MapClaims)
claims["purpose"] = "Just a test really"
signedToken, err := token.SignedString([]byte("NiceSecret"))
if err != nil {
// Internal Server Error if anything goes wrong in getting the signed token
fmt.Println(err)
return c.SendStatus(500)
}
cookie.Name = "access"
cookie.HTTPOnly = true
cookie.Secure = false
cookie.Domain = "localhost"
cookie.SameSite = "Lax"
cookie.Path = "/"
cookie.Value = signedToken
cookie.Expires = time.Now().Add(time.Hour * 24)
c.Cookie(cookie)
return c.Status(200).JSON(fiber.Map{
"message": "You have logged in",
})
}
What does this is basically look through JSON Placeholder's Users and if it finds one with a matching email, it sends the HTTPOnly Cookie with some data attached to it.
Seeing as it might be a problem with the library I'm using, I decided to write a Node version with Express.
import axios from 'axios'
import express from 'express'
import cookieParser from 'cookie-parser'
import jwt from 'jsonwebtoken'
const app = express()
app.use(express.json())
app.use(cookieParser())
app.use(express.urlencoded({ extended: true }))
app.disable('x-powered-by')
app.get("/", (req, res) => {
res.send("Hello there!")
})
app.post("/login", async (req, res, next) => {
try {
const { email } = req.body
const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users?email=${email}`)
if (data) {
if (data.length > 0) {
res.locals.user = data[0]
next()
} else {
return res.status(404).json({
message: "No results found"
})
}
}
} catch (error) {
return console.error(error)
}
}, async (req, res) => {
try {
let { user } = res.locals
const token = jwt.sign({
user: user.name
}, "mega ultra secret sauce 123")
res
.cookie(
'access',
token,
{
httpOnly: true,
secure: false,
maxAge: 3600
}
)
.status(200)
.json({
message: "You have logged in, check your cookies"
})
} catch (error) {
return console.error(error)
}
})
app.listen(8000, () => console.log(`Server is up at localhost:8000`))
Both of these do not work on the browsers I've tested them on.
Results
Go responds with this.
HTTP/1.1 200 OK
Date: Mon, 21 Feb 2022 05:17:36 GMT
Content-Type: application/json
Content-Length: 32
Vary: Origin
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Total-Count,Content-Range
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoiSnVzdCBhIHRlc3QgcmVhbGx5In0.8YKepcvnMreP1gUoe_S3S7uYngsLFd9Rrd4Jto-6UPI; expires=Tue, 22 Feb 2022 05:17:36 GMT; domain=localhost; path=/; HttpOnly; SameSite=Lax
For the Node API, this is the response header.
HTTP/1.1 200 OK
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiTGVhbm5lIEdyYWhhbSIsImlhdCI6MTY0NTQyMDM4N30.z1NQcYm5XN-L6Bge_ECsMGFDCgxJi2eNy9sg8GCnhIU; Max-Age=3; Path=/; Expires=Mon, 21 Feb 2022 05:13:11 GMT; HttpOnly
Content-Type: application/json; charset=utf-8
Content-Length: 52
ETag: W/"34-TsGOkRa49turdlOQSt5gB2H3nxw"
Date: Mon, 21 Feb 2022 05:13:07 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Client Source
I'm using this as a test form to send and receive data.
<script>
let email = "";
async function handleSubmit() {
try {
let response = await fetch(`http://localhost:8000/login`, {
method: "POST",
body: JSON.stringify({
email,
}),
headers: {
"Content-Type": "application/json",
},
});
if (response) {
console.info(response);
let result = await response.json();
if (result) {
console.info(result);
}
}
} catch (error) {
alert("Something went wrong. Check your console.");
return console.error(error);
}
}
</script>
<h1>Please Login</h1>
<svelte:head>
<title>Just a basic login form</title>
</svelte:head>
<form on:submit|preventDefault={handleSubmit}>
<label for="email">Email:</label>
<input
type="email"
name="email"
bind:value={email}
placeholder="enter your email"
/>
</form>
Additional Information
Postman: 9.8.3
Language Versions
Go: 1.17.6
Node.js: v16.13.1
Svelte: 3.44.0
Browsers Used
Mozilla Firefox: 97.0.1
Microsoft Edge: 98.0.1108.56
Chromium: 99.0.4781.0
I just had the same issue with axios, this was causing the Set-Cookie response header to be silently ignored. Which was annoying as usually if it rejects them it will show that little yellow triangle against that header and say why in the network inspector.
I solved this by adding a request interceptor to force it true for every request:
axios.interceptors.request.use(
(config) => {
config.withCredentials = true
return config
},
(error) => {
return Promise.reject(error)
}
)
Solution
It turns out the problem is in the front-end, specifically JavaScript's fetch() method.
let response = await fetch(`http://localhost:8000/login`, {
method: "POST",
credentials: "include", //--> send/receive cookies
body: JSON.stringify({
email,
}),
headers: {
"Content-Type": "application/json",
},
});
You'll need credentials: include property in your RequestInit object, not just for making requests that require cookie authentication, but for also receiving said cookie.
Axios usually fills this part out automatically (based from experience), but if it doesn't, you'll also need to put withCredentials: true on the third config argument of your request to allow the browser to set the cookie.
Related
I'm working through this tutorial on creating an app that uses the Spotify API. Everything was going great until I got to the callback portion of authenticating using the authentication code flow.
(I do have my callback URL registered in my Spotify app.)
As far as I can tell, my code matches the callback route that this tutorial and others use. Significantly, the http library is axios. Here's the callback method:
app.get("/callback", (req, res) => {
const code = req.query.code || null;
const usp = new URLSearchParams({
code: code,
redirect_uri: REDIRECT_URI,
grant_type: "authorization_code",
});
axios({
method: "post",
url: "https://accounts.spotify.com/api/token",
data: usp,
headers: {
"content-type": "application/x-www-form-urlencoded",
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`,
},
})
.then(response => {
console.log(response.status); // logs 200
console.log(response.data); // logs encoded strings
if (response.status === 200) {
res.send(JSON.stringify(response.data))
} else {
res.send(response);
}
})
.catch((error) => {
res.send(error);
});
Though the response code is 200, here's a sample of what is getting returned in response.data: "\u001f�\b\u0000\u0000\u0000\u0000\u0000\u0000\u0003E�˒�0\u0000Ee�uS\u0015��\u000e�(\b\u0012h\u0005tC%\u0010\u0014T\u001e�����0��^:���p\u0014Ѻ\u000e��Is�7�:��\u0015l��ᑰ�g�����\u0"
It looks like it's encoded, but I don't know how (I tried base-64 unencoding) or why it isn't just coming back as regular JSON. This isn't just preventing me logging it to the console - I also can't access the fields I expect there to be in the response body, like access_token. Is there some argument I can pass to axios to say 'this should be json?'
Interestingly, if I use the npm 'request' package instead of axios, and pass the 'json: true' argument to it, I'm getting a valid token that I can print out and view as a regular old string. Below is code that works. But I'd really like to understand why my axios method doesn't.
app.get('/callback', function(req, res) {
// your application requests refresh and access tokens
// after checking the state parameter
const code = req.query.code || null;
const state = req.query.state || null;
const storedState = req.cookies ? req.cookies[stateKey] : null;
res.clearCookie(stateKey);
const authOptions = {
url: 'https://accounts.spotify.com/api/token',
form: {
code: code,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
},
headers: {
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
json: true,
};
request.post(authOptions, function (error, response, body) {
if (!error && response.statusCode === 200) {
const access_token = body.access_token;
const refresh_token = body.refresh_token;
var options = {
url: 'https://api.spotify.com/v1/me',
headers: { Authorization: 'Bearer ' + access_token },
json: true,
};
// use the access token to access the Spotify Web API
request.get(options, function(error, response, body) {
console.log(body);
});
// we can also pass the token to the browser to make requests from there
res.redirect('/#' + querystring.stringify({
access_token: access_token,
refresh_token: refresh_token,
}));
} else {
res.redirect(`/#${querystring.stringify({ error: 'invalid_token' })}`);
}
});
});
You need to add Accept-Encoding with application/json in axios.post header.
The default of it is gzip
headers: {
"content-type": "application/x-www-form-urlencoded",
'Accept-Encoding': 'application/json'
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`,
}
I am creating an express app. I want to use cookies to save a jwt token. However, I am running into an issue where the browser receives a cookie but then doesn't send it back.
My stack is: express.js and svelte.js
My guess is, is that it is a cors error. I have tried:
setting allow-controll-access-credentials : true
SameSite : 'none'
On the client headers:
credentials : 'include'
mode : 'cors'
And for testing purposes, I have set 'httpOnly' to false, so I can check whether I actually received a cooke.
code for sending the cookie:
router.post("/cookie", (req, res) => {
if(req.body == null || req.body.emailAddress == null || req.body.password == null){
res.status(StatusCodes.BAD_REQUEST).json(ReasonPhrases.BAD_REQUEST)
} else {
const email = req.body.emailAddress
const password = req.body.password
const user = findSingleUserByEmail(email)
if (user && user.password) {
const result = bcrypt.compareSync(password, user.password)
if(result) {
const accessToken = jwt.sign({ email: user.emailAddress, role: user.role, username: user.username }, user.secret)
res.status(StatusCodes.CREATED)
.cookie('access_token', accessToken, cookieOptions)
.status(StatusCodes.CREATED)
.json(ReasonPhrases.CREATED)
}else {
res.status(StatusCodes.FORBIDDEN).json(ReasonPhrases.FORBIDDEN)
}
}else {
res.status(StatusCodes.FORBIDDEN).json(ReasonPhrases.FORBIDDEN)
}
}
})
fetch client
fetch(URL + PORT + '/auth/cookie', setRequest('POST', null, body))
.then(res => res.json())
.then(data => {
console.log(data)
$tokenStore = true
})
network tab output:
Set-Cookie: access_token=(cookie contents); Path=/; Expires=Wed, 28 Sep 2022 02:50:57 GMT
Finally, I was able to get it to work with Postman.
I have a vue web app that uses axios to communicate with an API. The authentication is handled by the server, and not by my app. That is, the server ensures that the user cannot see the app before they have authenticated.
Of course, after some time the user's authentication token expires and my app only notices this when it fires off a get/post request to the API. When this happens the axios request returns a redirect to a login page that, when printed to the console, looks something like this:
config: Object { url: "https://...url for my request...",
method: "get", baseURL: "...base url for api", … }
data: "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n<HTML>\n<HEAD>\n<TITLE>Need Authentication</TITLE>\n<link rel=\"stylesheet\" href=\"/Steely.css\" type=\"text/css\">\n</HEAD>\n<BODY>....</BODY>\n</HTML>\n"
headers: Object {
connection: "Keep-Alive",
"content-encoding": "gzip", "content-length": "1686", …
}
request: XMLHttpRequest {
readyState: 4, timeout: 0, withCredentials: false, …
}
status: 200
statusText: "OK"
<prototype>: Object { … }
app~d0ae3f07.235327a9.js:1:97292
What is the best way to redirect the user to this login page and then resume my original request? At the moment I am not even succeeding in recognising this. My axios code tries, and fails, to recognise when this happens and then redirect to user a vue component that has a login page. The relevant part of code looks like this:
export default new class MyAPI {
constructor() {
this.axios = axios.create({
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
},
baseURL: `https://.../api`,
});
}
// send a get request to the API
GET(command) {
return new Promise((resolve, reject) => {
this.axios.get(command)
.then((response) => {
if (response && response.status === 200) {
if ( response.data && typeof response.data == 'string' && response.data.includes('Require authentication') ) {
store.dispatch('authenticate', this.baseURL+'/'+command).then( (resp) => resolve(resp.data) )
} else {
resolve(response.data);
}
} else {
reject(response.data);
}
})
.catch((err) => { reject('Internal error'+err); });
});
}
}
This results in the dreaded
Internal errorTypeError: e(...) is undefined
error, although this error is almost certainly triggered further down the code since I not recognising the login authentication request.
Is anyone able to recommend how best to recognise and process the login request?
I'm new to passport JWT and have this code sending post request from client:
export function checkLogin() {
return function( dispatch ) {
//check if user has token
let token = localStorage.getItem('token');
if (token != undefined) {
//fetch user deets
var config = {
headers: {'authorization': token, 'content-type': 'application/json'}
};
axios.post(`${API_URL}/login`, null, config)
.then(response => {
console.log('response is:', response);
})
.catch((error)=> {
console.log(error);
});
}
//user is anonymous
}
and this request is sending off fine with token in headers like so:
Accept:application/json, text/plain, */*
Accept-Encoding:gzip, deflate
Accept-Language:en-US,en;q=0.8
authorization:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NzIzMTg2MDE5NTd9.SsCYqK09xokzGHEVFiHtHmq5_HvtWkb8EjQJzwR937M
Connection:keep-alive
Content-Length:0
Content-Type:application/json
DNT:1
Host:localhost:3090
Origin:http://localhost:8080
Referer:http://localhost:8080/
User-Agent:Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36
On the server side, it is correctly routed through passport and hits this code:
// setup options for JWT strategy
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('authorization'),
secretOrKey: config.secret,
ignoreExpiration: true
};
//create JWT strategy
const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done ) {
console.log('payload:',payload); // --> payload: { iat: 1472318601957 }
// see if the user ID in the payload exists in our database
User.findById(payload.sub, function(err, user) {
if (err) {
console.log('auth error');
return done( err, false );
}
//if it does, call 'done' function with that user
if (user) {
console.log('user authd:', user);
done( null, user);
//otherwise, call 'done' function without a user object
} else {
console.log('unauthd user'); //--> gets here only
done( null, false);
}
});
});
The problem is that the extractJwt function only returns the iat portion and not the sub portion which I need to check the db. What am I doing wrong?
OK I figured it out. I examined how my token generator function was working. I'm using mongoose and it was passing the id of the user model to the token like this:
function tokenForUser(user) {
const timestamp = new Date().getTime();
//subject and issued at time
return jwt.encode({ sub: user.id, iat: timestamp }, config.secret);
}
I looked at the actual model that was being sent into this function and it turns out that mongoose adds an id with the key _id not id. Once I changed it to this everything works!
function tokenForUser(user) {
const timestamp = new Date().getTime();
//subject and issued at time
const userid = user['_id'];
return jwt.encode({ sub: userid, iat: timestamp }, config.secret);
}
I am trying to authenticate using a webservice and make subequent api calls.
export class HomeComponent {
newName: string;
headers = {
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
};
constructor(public http: Http, public nameListService: NameListService) {
let _build = (<any>http)._backend._browserXHR.build;
(<any>http)._backend._browserXHR.build = () => {
let _xhr = _build();
_xhr.withCredentials = true;
return _xhr;
};
}
login(path, data) {
var creds = { 'Email': 'email#email.com', 'Password': 'pass', 'RememberMe': true };
this.http.post('http://xx.net/api/Users/Login', JSON.stringify(creds), this.headers)
.subscribe(res => {
console.log(res);
});
}
getdata() {
this.http.get('http://xx.net/api/Users/LoggedInUser', this.headers)
.subscribe(res => {
console.log(res);
}, (er) => {
console.log(er);
});
}
}
in the constructor i am setting _xhr.credentails to true.
Everything works great in chrome, But fails in IE and Safari.
When i try to login(in all browser), i can see the response header with "Set-Cookie", But safari and IE does not seems to send this cookie with subsequent requests so i get 401. Chrome does this correctly.
is this a bug with the framework.
Might be a dup of Issue in this.withCredentials attribute in Cross Origin Resource Sharing
Steps for allowing 3rd party cookies:
IE 10 - Internet Options > Privacy > Advanced > Third Party Cookies > Accept
Safari - Preferences > Privacy > Block Cookies > Never