httponly cookie not available in subsequent xhr requests - xmlhttprequest

Background
I have a restful backend, React + Redux frontend and I'm trying to protect against CSRF and XSS attacks.
The frontend requests a CSRF token from the API. The API response sets the CSRF token in a HttpOnly cookie and also in the response body. The redux reducer saves the token (from the response body) to the redux store.
If I request the token in the main container's componentDidMount(), everything works, but the concern is this is a one shot. Instead, as the requests to the API go through a custom middleware, I would prefer the middleware to request the CSRF token if it doesn't exist locally.
Issue
The flow is as follows (Tested on Chrome 50 and Firefox 47):
CSRF token requested. Token stored in HttpOnly cookie and redux store
Original API call requested with X-CSRF-Token header set. cookie not sent
Receive 403 from API due to missing cookie. API responds with new HttpOnly cookie. The Javascript can't see this cookie, so the redux store is not updated.
Additional API calls requested with X-CSRF-Token header from step 2. and cookie from step 3.
Receive 403 due to mismatched cookie vs X-CSRF-Token
If I add a delay before step 2 with window.setTimeout, the cookie is still not sent, so I don't think it's a race condition with the browser not having enough time to save the cookie?
Action Creator
const login = (credentials) => {
return {
type: AUTH_LOGIN,
payload: {
api: {
method: 'POST',
url: api.v1.auth.login,
data: credentials
}
}
};
};
Middleware
/**
* Ensure the crumb and JWT authentication token are wrapped in all requests to the API.
*/
export default (store) => (next) => (action) => {
if (action.payload && action.payload.api) {
store.dispatch({ type: `${action.type}_${PENDING}` });
return ensureCrumb(store)
.then((crumb) => {
const state = store.getState();
const requestConfig = {
...action.payload.api,
withCredentials: true,
xsrfCookieName: 'crumb',
xsrfHeaderName: 'X-CSRF-Token',
headers: {
'X-CSRF-Token': crumb
}
};
if (state.auth.token) {
requestConfig.headers = { ...requestConfig.headers, Authorization: `Bearer ${state.auth.token}` };
}
return axios(requestConfig);
})
.then((response) => store.dispatch({ type:`${action.type}_${SUCCESS}`, payload: response.data }))
.catch((response) => store.dispatch({ type: `${action.type}_${FAILURE}`, payload: response.data }));
}
return next(action);
};
/**
* Return the crumb if it exists, otherwise requests a crumb
* #param store - The current redux store
* #returns Promise - crumb token
*/
const ensureCrumb = (store) => {
const state = store.getState();
return new Promise((resolve, reject) => {
if (state.crumb.token) {
return resolve(state.crumb.token);
}
store.dispatch({ type: CRUMB_PENDING });
axios.get(api.v1.crumb)
.then((response) => {
store.dispatch({ type: CRUMB_SUCCESS, payload: { token: response.data.crumb } });
window.setTimeout(() => resolve(response.data.crumb), 10000);
// return resolve(response.data.crumb);
})
.catch((error) => {
store.dispatch({ type: CRUMB_FAILURE });
return reject(error);
});
});
};

This was caused because I was creating a new axios client on each request, if I reuse the same axios client for all API requests, the cookie is saved correctly and used in subsequent requests.

Related

How to redirect to login page if JWT is not valid, using Express with API

I've built an Express app that contains an API and a front end. By using Axios the front end can request data (e.g. a user-object or a todo-object) from the API, which will validate the offered JWT with its middleware. If the jwt.verify() errs, the protected routes won't fire. This all works fine.
My question is: how do I set up the front end such that any page-request will redirect to a login page if the browser-stored JWT is not valid (excluding the login and register pages, to prevent circular redirection)? Do I have to preface every .ejs-file with an Axios.post() that sends the browser-stored JWT for verification, or is there a best practice that I am missing?
My goal, when an invalid JWT is offered, is to have the API routes return a json-object (e.g. { err: "invalid token offered" }), and to have all the front end routes redirect the user to the login page.
Some sample code below.
server.js
// API Routes
app.use('/api/todos', CheckToken, APITodosRouter)
app.use('/api/auth', APIAuthRouter)
// Front-end Routes
app.use('/', indexRouter)
app.use('/todos', todosRouter)
app.use('/auth', authRouter)
todos.ejs (This works fine)
// get todos from db
let todosData
const getTodos = async () => {
let response = await axios.get('/api/todos/all', {
headers: {
'Content-Type': 'application/json',
'authorization': `Bearer ${localStorage.access_token}`
}
})
if (!response) return console.log({ msg: "no response received."})
if (!response.data) return console.log({ msg: "no data received."})
if (!response.data.payload) return console.log({ msg: "no todos found."})
todosData = response.data.payload
}
// boot page
;(async () => {
await getTodos()
renderTodos() // a function that reads todosData updates the DOM accordingly
})()
checkToken.js (Middleware)
const jwt = require('jsonwebtoken')
const checkToken = (req, res, next) => {
const ah = req.headers.authorization
const token = ah && ah.split(' ')[1]
if (!token) return res.json({ msg: "No token offered."})
jwt.verify(token, process.env.TOKEN_SECRET, (err, user) => {
if (err) return res.json({ msg: "Invalid token offered."})
req.user = user
next()
})
}
module.exports = checkToken

React native set cookie on fetch/axios

I am trying to set the cookie on fetch or axios, I already checked the solutions posted on github or stackoverflow, but none of them are working now.
I'm using Saml for authentication on my RN project.
So Here are stories:
on the first login, if the user clicks the start button, it calls the api of get profile info, if there is no cookie on header, it returns redirect url and also cookie(it's unauth cookie), and go to the url on the webview, after the user logins on the webview, then the original url(get profile api) is called on webview, after that, I'd grab the auth cookie using react-native-cookies library, and then set it on the header of fetch/axios. but it doesn't work.
export async function getMyProfile() {
const cookies = await LocalStorage.getAuthCookies();
await CookieManager.clearAll(true)
const url = `${Config.API_URL}/profiles/authme`;
let options = {
method: 'GET',
url: url,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true
};
if (cookies) options.headers.Cookie = cookies.join(';')
return axios(options)
.then(res => {
console.info('res', res);
return res;
}).catch(async (err) => {
if (err.response) {
if (err.response.status === 401) {
const location = _.get(err, 'response.headers.location', null);
const cookie = _.get(err, 'response.headers.set-cookie[0]', null);
await LocalStorage.saveUnAuthCookie(cookie);
return { location, cookie, isRedirect: true };
}
}
});
}
You could use Axios interceptor.
let cookie = null;
const axiosObj = axios.create({
baseURL: '',
headers: {
'Content-Type': 'application/json',
},
responseType: 'json',
withCredentials: true, // enable use of cookies outside web browser
});
// this will check if cookies are there for every request and send request
axiosObj.interceptors.request.use(async config => {
cookie = await AsyncStorage.getItem('cookie');
if (cookie) {
config.headers.Cookie = cookie;
}
return config;
});

Problem setting API client authorization key in axios

The API is developed using Laravel, I am currently implementing authorization logic using Laravel Passport. the client application is a Vuejs application, Http calls are done using axios.
Passport is perfectly returning a token (i'm using client credentials type of grants). axios offers a way to set default headers by setting axios.defaults.headers.common array. Here is my axios call (implemented in bootstrap.js)
async function a() {
var ret = "";
await axios
.post("/oauth/token", {
"client_id": 7,
"client_secret": "2GmvfxQev7AnUyfq0Srz4jJaMQyWSt1iVZtukRR6",
"grant_type": "client_credentials",
"scope": "*"
})
.then((resp) => {
ret = resp.data.access_token;
})
return ret;
}
a().then((res) => {
console.log(res) //this perfectly loggs the token to the console.
axios.defaults.headers.common["Authorization"] = "Bearer " + res
})
However, all subsequent axios calls are missing the Bearer token header.
You may try to create an axios instance with custom config:
https://github.com/axios/axios#creating-an-instance
Example:
const axios = require('axios').create({
headers: {
'Authorization': 'Bearer: ' + token
}
});
and use it just like you would normally do:
axios.get(url).then(resp => {
//response handler
});
axios.post(url, data).then(resp => {
//response handler
});

AWS-amplify Including the cognito Authorization header in the request

I have create an AWS mobile hub project including the Cognito and Cloud logic. In my API gateway, I set the Cognito user pool for the Authorizers. I use React native as my client side app. How can I add the Authorization header to my API request.
const request = {
body: {
attr: value
}
};
API.post(apiName, path, request)
.then(response => {
// Add your code here
console.log(response);
})
.catch(error => {
console.log(error);
});
};
By default, the API module of aws-amplify will attempt to sig4 sign requests. This is great if your Authorizer type is AWS_IAM.
This is obviously not what you want when using a Cognito User Pool Authorizer. In this case, you need to pass the id_token in the Authorization header, instead of a sig4 signature.
Today, you can indeed pass an Authorization header to amplify, and it will no longer overwrite it with the sig4 signature.
In your case, you just need to add the headers object to your request object. For example:
async function callApi() {
// You may have saved off the JWT somewhere when the user logged in.
// If not, get the token from aws-amplify:
const user = await Auth.currentAuthenticatedUser();
const token = user.signInUserSession.idToken.jwtToken;
const request = {
body: {
attr: "value"
},
headers: {
Authorization: token
}
};
var response = await API.post(apiName, path, request)
.catch(error => {
console.log(error);
});
document.getElementById('output-container').innerHTML = JSON.stringify(response);
}
Tested using aws-amplify 0.4.1.

Automatically log out user when token is invalidated

I have a SPA that is built on vuejs. When a user is logged in via API, the token is stored in local storage.
I need a global solution which will logout and prompt the user when the token is no longer valid. At the moment, I get "invalid token" error when accessing private API endpoints.
How do I rig axios so that ALL response of invalid tokens will trigger the logout/prompt code?
Here is an simple example with axios. It use a Bearer token for authentification.
import axios from "axios";
import { useUserStore } from "#/store/userStore";
const apiClient = axios.create({
baseURL: ""http://127.0.0.1:8001",
headers: {},
});
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const config = error?.config;
if (error?.response?.status === 401) {
const result = await refreshToken();
if (result) {
config.headers = {
...config.headers,
authorization: `Bearer ${result?.token}`,
};
}
return axios(config);
}
);
const refreshToken = async () => {
/* do stuff for refresh token */
// if refresh token failed
try {
useUserStore().actionLogout();
} catch (error) {
console.log(error);
} finally {
loacalStorage.clear();
}
};
you can write a function that clears your local storage after some time and logout user