I've had csrf protection with the csurf module working for a while now on my React SPA. I am also using passport for authentication. I do not do any server-side rendering, so the server sends a csrf token in the response body to the client when it hits the /users/current endpoint, which is protected with csrfProtection, something like this:
import csrf from 'csurf';
const csrfProtection = csrf();
router.get("users/current", csrfProtection, async function(req, res)
{
.....
res.write(JSON.stringify({ ..., csrfToken: req.csrfToken() }));
res.end();
}
On the client side I then add the token to all subsequent request headers, a bit like this:
axiosInstance.get("/users/current")
.then(resJson =>
{
axiosInstance.interceptors.request.use(config =>
{
config.headers["x-csrf-token"] = resJson.data.csrfToken;
return config;
});
}
My first question is how the first request even manages to pass the csrfProtection without a token in its header. Yet since the token can only be accessed on the server to send to the client if the route is csrf protected, I don't see a way around this, and it does work somehow.
However, recently I have been getting "ForbiddenError: invalid csrf token" when a user logs in or deletes their account. This has only started happening after I upgraded all my node packages to the latest versions. First the client makes a request to /users/login to submit the username & password, and then makes a request to /users/current to get the new csrf token:
axiosInstance.post("/users/login", {
"username": login.username,
"password": login.password
})
.then(async resJson =>
{
// *code to update user details in redux store*
// ......
axiosInstance.interceptors.request.use(config =>
{
config.headers["x-csrf-token"] = undefined;
return config;
});
return resJson;
})
.then(async resJson =>
{
const { csrfToken } = await axiosInstance.get("/users/current")
.then(resJson => resJson.data);
axiosInstance.interceptors.request.use(config =>
{
config.headers["x-csrf-token"] = csrfToken;
return config;
});
return resJson.data;
}
I suspect it's something to do with subsequent requests coming from a different userId (which I obtain from req.user[0].userId), with which csurf will not accept the previously issued token. But I have no idea how to issue the new token csurf does expect, to the client. And it still doesn't explain why what I had before has suddenly stopped working since none of my logic has changed. This isn't the kind of error I'd typically expect after package updates.
Here someone mentions you can just set any header on the client and have the server check for that. atm I am adding the csrf token to all the client's request headers and using the csurf module's request handler function to check it, but there is nothing stopping me from writing my own. If this is true, the value of the header doesn't even matter, just that it exists. I am holding off on this option though because I feel there is something basic I'm not understanding about my current setup, which once rectified will mean this can be easily fixed.
Would appreciate any help or explanation! Thanks 🤍
Related
Premise / What you want to achieve
React x Redux (port: 3000)
Go (port: 8080)
I am making a SPA.
I run into a CROS error when hitting the Go API.
I've encountered this problem many times, and every time I think it's solved, I hit a new API.
I should have made the basic settings, but I'm in trouble because I don't know what caused it.
We would appreciate it if you could help us.
Problem / Error message
Access to XMLHttpRequest at'http://localhost:8080/login' from origin'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No'Access-Control -Allow-Origin'header is present on the requested resource.
I encountered this when I hit the login API (post).
However, when I encountered this problem several times, I set cros on the header of api and axios side, and
Another get API avoided the error.
Also, when you hit api with postman, it becomes header
We have also confirmed that the header set in Go such as Allow-Origin is given without any problem.
Applicable source code
Header settings in Go
w.Header (). Set ("Content-Type", "application /json")
w.Header (). Set ("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header (). Set ("Access-Control-Allow-Credentials", "true")
react axios settings
axios.defaults.baseURL ='http://localhost:8080';
axios.defaults.headers.post ['Content-Type'] ='application/json';
Posting code with an error
export const signIn = (email, password) => {
return async (dispatch) => {
try {
const response = await axios.post ('/login', {
email: email,
password: password,
});
const data = response.data;
dispatch (
signInAction ({
isSignedIn: true,
})
);
} catch (error) {
console.log (error);
}
};
};
Code hitting a successful getapi
useEffect (() => {
async function fetchTickers () {
try {
const response = await axios.get (`/ticker?Symbol=^skew`);
const data = response.data;
setChartAry ([... chartAry, [... data.daily]]);
} catch (error) {
console.log (error);
setChartAry ([]);
}
}
fetchTickers ();
}, []);
What I tried
I tried all the solutions that hit with stackoverflow etc. Also, considering the possibility of a problem with the browser itself, we also cleared the cache.
Is it the difference between axios by get and post? And how should I debug it?
I had this problem some time ago but I used Express for the backend, who knows this can solve your problem too.
try adding this to the axios settings axios.defaults.withCredentials = true;
You also need to allow the OPTIONS method for preflight requests
this article might help you solve the CORS problem on the backend: https://flaviocopes.com/golang-enable-cors/
The method was validated in gorilla / mux.
- r.HandleFunc("/login", app.loginHandler).Methods("POST")
+ r.HandleFunc("/login", app.loginHandler).Methods("POST", "OPTIONS")
We also had to deal with preflight.
if r.Method == "OPTIONS" {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
return
}
I am trying to test my React (v16.10.2) application with Cypress (v4.5.0). Our application is using Okta for authentication (with the client #okta/okta-react 1.3.1).
I can use my app from the browser without any issues. The first time I login, I get the Okta login screen. I enter my user ID and password, and the react client calls the authn endpoint with my creds, and then calls the authorization endpoint to get the token. I am then taken to the first screen of our application.
When I try to login in my Cypress test, my user ID and password are entered into the login screen, the authn endpoint is called successfully, but the authorization endpoint returns a 403 error. Unfortunately, there is no other info about why I am getting the 403.
I have compared the authorization requests between the one that works in the browser, and the one that doesn't work from Cypress. The only real difference I see is that the working browser request has an origin header, whereas the failing one does not.
Question #1: Could the missing origin header be the cause of my problem?
In order to avoid a bunch of CORS and cross-site issues, I had to install a couple Chrome extensions (ignore-x-frame-headers and Access-Control-Allow-Origin-master). I am implementing them in the following code in cypress/plugins/index.js:
module.exports = (on, config) => {
on('before:browser:launch', (browser = {}, launchOptions) => {
// The following code comes from https://medium.com/#you54f/configuring-cypress-to-work-with-iframes-cross-origin-sites-afff5efcf61f
// We were getting cross-origin errors when trying to run the tests.
if (browser.name === 'chrome') {
const ignoreXFrameHeadersExtension = path.join(__dirname, '../extensions/ignore-x-frame-headers');
launchOptions.args.push(`--load-extension=${ignoreXFrameHeadersExtension}`);
const accessControlAllowOriginMasterExtension = path.join(__dirname, '../extensions/Access-Control-Allow-Origin-master');
launchOptions.args.push(`--load-extension=${accessControlAllowOriginMasterExtension}`);
launchOptions.args.push("--disable-features=CrossSiteDocumentBlockingIfIsolating,CrossSiteDocumentBlockingAlways,IsolateOrigins,site-per-process");
launchOptions.args.push('--disable-site-isolation-trials');
launchOptions.args.push('--reduce-security-for-testing');
launchOptions.args.push('--out-of-blink-cors');
}
if (browser.name === 'electron') {
launchOptions.preferences.webPreferences.webSecurity = false;
}
return launchOptions;
});
I also added the following to cypress.json:
{
"chromeWebSecurity": false
}
Here is my cypress test:
describe('Order Lookup Test', () => {
const UI_URL: string = 'http://localhost:3000/';
const ORDER_NUMBER: string = '10307906234';
beforeEach(() => {
Cypress.config('requestTimeout', 50000);
cy.visit(UI_URL);
cy.get('#okta-signin-username', {timeout: 10000}).type('xxxxxxxx');
cy.get('#okta-signin-password', {timeout: 10000}).type('xxxxxxxx');
cy.get('#okta-signin-submit', {timeout: 10000}).click();
})
it('should return an order', () => {
cy.get('.number-input', {timeout: 10000}).type(ORDER_NUMBER);
cy.get('.order-lookup-buttons-search-valid').should('be.visible').click();
})
})
Does anyone have any idea what might be going on? What other information should I be including in order to help narrow this down?
I have a ReactJS app running in browser, which needs access to my backend laravel-passport API server. So, I am in control of all code on both client and server side, and can change it as I please.
In my react app, the user logs in with their username and password, and if this is successful, the app recieves a personal access token which grants access to the users data. If I store this token in local storage, the app can now access this users data by appending the token to outgoing requests.
But I do not want to save the access token in local storage, since this is not secure. How do I do this?
Here is what I have tried:
In the laravel passport documentation, there is a guide on how to automatically store the access token in a cookie. I believe this requires the app to be on the same origin, but I cannot get this to work. When testing locally, I run the app on localhost:4000, but the API is run on my-app.localhost. Could this be a reason why laravel passport does not make a cookie with the token, although they technically both have origin localhost?
OAuth has a page on where to store tokens. I tried the three options for "If backend is present", but they seem to focus on how the authorization flow rather than how to specifically store the token.
Here's the relevant parts of my code (of course, feel free to ask for more if needed):
From my react app:
const tokenData = await axios.post(this.props.backendUrl + '/api/loginToken', { email: 'myEmail', password: 'myPassword' })
console.log('token data: ', tokenData)
const personalAccessToken = tokenData.data.success.token;
var config = {
headers: {
'Authorization': "Bearer " + personalAccessToken
};
const user = await axios.get(this.props.backendUrl + '/api/user', config);
From the controller class ApiController:
public function loginToken()
{
if (Auth::attempt(['email' => request('email'), 'password' => request('password')])) {
$user = Auth::user();
$success['token'] = $user->createToken('MyApp')->accessToken;
return response()->json(['success' => $success], 200);
} else {
return response()->json(['error' => 'Unauthorised'], 401);
}
}
and the loginToken function is called from the /api/loginToken route.
Expected and actual results:
Ideally, I would love to have the token saved in a cookie like in the passport documentation, so I don't even have to attach the token to outgoing requests from the react app, but I'm not sure that this is even possible. Perhaps with third party cookies?
Else, I'd just like to find some way to store the token securely (for example in a cookie?), and then append it to outgoing calls from the react app.
I've been stuck on this issue for a while now, I'm using ADAL.js on the front-end to handle login and authentication. Once logged in I need to get the info for the user (roles, groups, name etc...) however I can't get anything back from the /adfs/userinfo endpoint other than a 401.
So far I log the user in and get back an id_token and access token (or "adal.access.token.key{guid}" in the browser) which is identical to the access key. Due to a cors issue on the front-end I then send this to a back-end mvc core 2.2 controller to make the call to /adfs/userinfo which is where I get the 401. Javascript code below
this.adalAuthentication.Instance.acquireToken(this.adalAuthentication.Instance.config.clientId, (error, token) => {
if (error || !token) {
console.log('ADAL Error Occurred: ' + error);
return;
}
axios({
method: 'get',
url: '/identity/completeLogin/' + token,
headers: {
'Authorization': 'Bearer ' + token
}
}).then((response) => { console.log(response.data) });
});
And controller action...
[HttpGet("completeLogin/{access_token}")]
public async Task<HttpResponseMessage> CompleteLogin(string access_token)
{
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth" + access_token);
var response = await client.GetAsync("https://adfs.server/adfs/userinfo");
response.EnsureSuccessStatusCode();
try
{
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
return response;
}
catch (Exception e)
{
throw(e);
}
}
At this point I'm stumped, I'm thinking I either can't use ADAL for this or perhaps need to use oidcinstead of OAuth/jwt but I don't want to have to rewrite lots just to find out that doesn't work either or there's a better/best practice way of doing it. Has anyone had this issue before and/or can point me in the right direction or can see where I'm going wrong?
Other things I've tried;
hitting adfs/oauth/token endpoint (just returns with adfs server error)
setting Authorisation on the backend to client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer" + access_token); (just returns invalid token).
Making the front and backend a post method and using the getCachedToken method on the ADAL AuthenticationContext
EDIT: I also have this question open with a the slightly more specific goal of getting an access token with the id token
There's a Postman sample here.
Be aware that "userinfo" only returns a "sub" claim.
I am coding a SPA in react.js and I am using redux-api to handle backend connection. I want to do a sync action to refresh the auth token before doing the main action; this way, every time I will do an action to the backend I will be sure that the token is valid.
const endpoints = {
{
url: '/some/url',
crud:true,
prefetch:[
({actions, dispatch, getState}, cb) =>{
actions.auth_token.post(JSON.stringify({
token: "my token",
refreshToken: "my_refresh_token"
}),null, (err, data) =>{
if(err){
// HANDLE ERROR
}
setToken(data)
})
}
]
}
}
const api = reduxApi(endpoints)
How can I call the prefetch function in a sync way? So first the token refreshes and then the Action?
EDIT
We can do the stuff async, the important is the final call to cb(), here is the example
const endpoints = {
{
url: '/some/url',
crud:true,
prefetch:[
({actions, dispatch, getState}, cb) =>{
let mills = new Date().getTime()
const { token, generationTime, accessTokenLife, refreshTokenLife, refreshToken } = localStorage
// Conditions: exixts token, it is expired, refresh token is not expired
if(token && generationTime + accessTokenLife - 500 < mills && generationTime + refreshTokenLife - 500 > mills){
dispatch(actions.token_refresh.get(null, null, (err, data) =>{
if(err){
dispatch(setError(err))
}else{
refreshTokenData(data)
}
cb()
}))
}else{
cb()
}
}
]}}
const api = reduxApi(endpoints)
You may not need to request the token every time you do an async action. In fact, I'd encourage you not to.
You can request the token when you authenticate the user and cache it using web storage. Now instead of sending a network request to retrieve the users token every time you need it, you simply check the browsers cached storage. If the token for the user exists then the user has successfully authenticated. Otherwise, the user has not logged in and you can redirect the user to the authentication page.
Since that was not actually an answer to your problem but rather a different way to solve your problem I will also answer your question in a way that is more inline with the question. You should be able to utilize promise chaining to request the user's token and then once that resolves, do any other action.
I will explain in an abstract way that is not explicity related to redux-api that you should be able to adapt to redux-api specific constructs easy enough.
const actionOne = () => {
actions.post(myJson)
.then(response => actionTwo(response))
.catch(error => console.log(error))
}
An important modification you would need to make is to convert actions.auth_token.post to return a promise. Then you can chain other actions to the resolution of that promise. If you are not familiar with promises MDNs documentation is quite good. For more information on converting a function from callbacks to promises this Stack Overflow answer is quite detailed.