I am using localstorage to store a jwt in my react/redux app for authentication. I am trying to have the user get logged out if their token is expired. One way I have gotten this to work would be to use my authMiddleware.js on the backend to send the error.message and set the payload equal to an error variable, and then in a useEffect if the error is jwt expired I run my logout function (which just clears the localstorage) and reset the error to null. Like the following:
authMiddleware.js:
const jwt = require("jsonwebtoken");
const User = require("../models/user");
const protect = async (req, res, next) => {
let token = req.body.token;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select("-password");
next();
} catch (error) {
res.status(400).json({ message: error.message });
}
};
module.exports = { protect };
Portfolio.slice:
export const getCurrentHoldings = createAsyncThunk(
"/portfolio/getCurrentHoldings",
async (value, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
const userID = thunkAPI.getState().auth.user._id;
const newObj = {
token: token,
userID: userID,
};
let url = `http://localhost:3001/api/portfolio/getCurrentHoldings`;
const response = await axios.post(url, newObj);
console.log("New request ran in getCurrentHoldings");
return response.data;
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
const initialState = {
error: null,
status: "idle",
holdings: null,
jwtError: null,
};
export const portfolioSlice = createSlice({
name: "portfolio",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers(builder) {
builder
.addCase(getCurrentHoldings.pending, (state, action) => {
state.status = "loading";
})
.addCase(getCurrentHoldings.fulfilled, (state, action) => {
state.status = "success";
state.holdings = action.payload;
console.log(state.holdings);
})
.addCase(getCurrentHoldings.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
state.jwtError = action.payload;
})
},
});
Portfolio.js:
useEffect(() => {
if (jwtError == "jwt expired") {
dispatch(logout());
dispatch(reset());
}
}, [jwtError]);
The problem with this solution is I have multiple slices that I would need to add a similar variable for each and the useEffect would grow and start looking like:
useEffect(() => {
if (jwtError == "jwt expired") {
dispatch(logout());
dispatch(reset());
}
if (jwtError1 == "jwt expired") {
dispatch(logout());
dispatch(reset1());
}
if (jwtError2 == "jwt expired") {
dispatch(logout());
dispatch(reset2());
}
}, [jwtError, jwtError1, jwtError2]);
Thus this solution is not scalable, one way I thought to fix this was having some of the slices access data from another slice so at least the useEffect would be reduced to the original size and be scalable but I found that reducers only have access to the state they own Thus looking into this problem more I found a couple of posts related to this and I got suggestions to either 1. use cookies instead of localstate 2. Use middleware and 3. use instance.interceptors
Now one question I had for all of the above solutions is if this issue should be solved on the frontend, backend, or both? Since the middleware and instance.interceptors solution looks like its solved on the frontend. I would like to know if this is a security risk and if you should also use a backend middleware aswell.
I also would like to know if using cookies instead of useState is just a best practice, but either way I would like to implement this with localstorage also.
And finally I would like a best practices for how this should be done with redux in react and what the code might look like with my setup.
Update:
The solution I am trying currently is redux middleware and I am unable to decode the token on the frontend, installing jsonwebtoken in the react project results in a an error: Module not found: Error: Can't resolve 'crypto' in myfile. As far as I know I will need this library on the frontend if I am to decode it as suggested in middleware link.
Thus looking into this problem more I found a couple of posts related to this and I got suggestions to either 1. use cookies instead of localstate 2. Use middleware and 3. use instance.interceptors
The suggestions you got are great, I would definitely use an http-only cookie to store the token (safer because separated from the JS runtime, no malicious js code can ever see it) and a redux middleware and an axios interceptor.
The solution I am trying currently is redux middleware and I am unable to decode the token on the frontend, installing jsonwebtoken in the react project results in a an error: Module not found: Error: Can't resolve 'crypto' in myfile. As far as I know I will need this library on the frontend if I am to decode it as suggested in middleware link.
If you're using https://www.npmjs.com/package/jsonwebtoken, this seems to be a Node.js-only implementation, not meant for the browser. Looking at JWT Verify client-side? suggests that https://github.com/auth0/jwt-decode should be sufficient for you in the browser.
If you don't go with a http-only cookie based solution, there is a more elegant solution: You can decode the JWT, read the expiration time, and then schedule a function to run a few seconds before the expiration time (via setInterval) that refreshes the token. If this fails, the function can dispatch an action that logs the user out and resets the redux state to what you need it to be. This is a more proactive solution, as you don't need to wait until a request to the backend fails because of an expired token - after all you know when it will expire.
Related
const ForgotPassword = ({ ...others }) => {
const theme = useTheme();
const { token } = useParams()
const handleSubmit = async(values) => {
console.log('load')
try {
const response = await axios.post(`http://localhost:3001/auth/reset/${token}`, {
password: values.password
});
if (response.data.msg === 'Password reset token is invalid or has expired') {
console.log(response)
} else {
// success message
}
} catch (err) {
console.error(err);
console.log("Something went wrong. Please try again later.")
}
console.log('not load')
};
return (
//...Content...
);
};
export default ForgotPassword;
I need to export token to use in other files how to do ?
I tried like this
export const token = useParams().token;
export const Token = token
etc ..
I'm new to this, could anyone tell me how to export token?
You can't export token directly because it's a local variable and is not available to export at the start of your app. Depending on where token needs to be accessed, you can pass it to a child component with a prop. If this fits your use case, it'll be the easiest method.
If you instead need to access the data from adjacent or parent components, you can set up an application store and access it from parent components. Something like React Redux will do this for you. Take a look at the React Redux Quick Start page for details on how that might work. This is my preferred method, but you can also use React Context instead of an external library to accomplish the same task.
I am integrating payment api, I'm using firebase cloud functions as my backend and react-native in frontend, so right now i have the following code:
In react-native side:
const PaymentScreen = () => {
setLoading(true);
const onPay = () => {
const onPaymentCall = firebase.functions().httpsCallable('onPayment');
onPaymentCall(productsData)
.then(res => {
setLoading(false);
if (res?.data?.statusCode === 200) {
const paymentReceivedData = {
transactionId: res?.data?.transactionId,
paymentDate: new Date().now(),
totalPrice: res?.data?.totalPrice
}
navigator.navigate('ReceiptScreen', { paymentReceivedData });
}
})
}
return (
<TouchableOpacity onPress={onPay}>PAY</TouchableOpacity>
)
}
firebase function to handle payment:
export const onPayment = functions
.runWith({ timeoutSeconds: 300 })
.https.onCall(async (data: any, context: any) => {
if (!context.auth) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called while authenticated.'
);
}
try {
const paymentResult = await paymentThroughApi(data);
await admin.firestore().collection('wallet').add({...paymentResult});
const response = {
statusCode: 200,
totalPrice: paymentResult.totalPrice,
transactionId: paymentResult.transactionId,
}
return response;
} catch (error) {
const response = {
statusCode: 400,
message: 'Payment unsuccessful',
transactionId: null,
}
return response;
}
So the question is, how can I handle long-time response/timeout error/network-loss etc in react-native side, like how should I make it more robust? especially handling long-time responses, timeout errors, payment failures etc? Which packages should I use? like NetInfo to check internet connectivity but what is the best way to make payment flow robust?
So can anyone guide me or suggest any code snippets to add on to my current code?
Thank you!
I don't think there is one definite answer to your question, But here is what I think:
For managing network connection status in React Native you can use
NetInfo for information about network connection and Axios to make network requests to a public API as per documentation and for more information check with this Stackoverflow Link.
For timeout errors you can check the Article from Dan Abromov, which states Making setInterval Declarative with React Hooks and with this link
There are different answers for long-time responses in react-native.
After a form submission using Redux, I am able to see the plain text password in the dev tools meta section. Is this safe? Am I doing something wrong when passing the password down to the reducer? How can I make this more secure?
So in my userSlice I am creating an Async Thunk that accepts user input then grabs the user from my server/database.
export const setUserAsync = createAsyncThunk(
'user/setUserAsync',
async (payload, { rejectWithValue }) => {
try {
const response = await axios.post('/auth/login', payload);
const { token, user } = response.data;
console.log(response);
localStorage.setItem('user', JSON.stringify(user));
localStorage.setItem('token', token);
return user;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
which works as intended. I am then calling the fulfilled reducer to set the user state.
[setUserAsync.fulfilled]: (state, action) => {
state.user = action.payload;
state.isLoggedIn = !!action.payload;
}
but in my dev tools I am seeing the following which is plain text of the password I input, in this case it is wrong, but when it's right it shows it just the same.
I don't think you need to be concerned. The production bundle of your app won't have the redux devtools enabled so the password can't linger there. And if you're using proper TLS (see https://security.stackexchange.com/questions/110415/is-it-ok-to-send-plain-text-password-over-https ), the password remains encrypted.
I'm using NuxtJS's auth module and trying to get the Bearer token and a custom cookie that contains a sessionType on nuxtServerInit so I can update the store with a mutation, but it only works when I reload the page.
If I close the browser and go directly to my app url, I keep getting undefined for auth._token.local because nuxtServerInit executes before the cookies are ready.
My code in store/index.js looks like this:
export const actions = {
async nuxtServerInit({ commit, dispatch }, { req }) {
// Parse cookies with cookie-universal-nuxt
const token = this.$cookies.get('token')
const sessionType = this.$cookies.get('sessionType')
// Check if Cookie user and token exists to set them in 'auth'
if (token && user) {
commit('auth/SET_TOKEN', token)
commit('auth/SET_SESSION_TYPE', user)
}
}
}
I'm using nuxt-universal-cookies library.
What's the way to execute the action after the cookies are loaded on the browser?
Having it work with F5 and not by hitting enter makes me suspect that it just works sometimes and sometimes it doesn't, because F5 and Enter should trigger same behaviour on Nuxt (apart from some cache headers).
The only suspicious thing about you code is the usage of an async function when the function is not returning or awaiting any promise.
So you either await for an action
export const actions = {
async nuxtServerInit({ commit, dispatch }, { req }) {
// Parse cookies with cookie-universal-nuxt
const token = this.$cookies.get('token')
const sessionType = this.$cookies.get('sessionType')
// Check if Cookie user and token exists to set them in 'auth'
if (token && user) {
await dispatch('SET_SESSION', {token, user})
//commit('auth/SET_TOKEN', token)
//commit('auth/SET_SESSION_TYPE', user)
}
}
}
or you remove the async from the declaration
export const actions = {
nuxtServerInit({ commit, dispatch }, { req }) {
// Parse cookies with cookie-universal-nuxt
const token = this.$cookies.get('token')
const sessionType = this.$cookies.get('sessionType')
// Check if Cookie user and token exists to set them in 'auth'
if (token && user) {
commit('auth/SET_TOKEN', token)
commit('auth/SET_SESSION_TYPE', user)
}
}
}
I've had the same issue and found out that nuxtServerInit is triggered first before the cookie was set like via a express middleware.
High-level description
I have a React/redux/electron app that uses Google Oauth. I want to be able to refresh the access token automatically when it expires. I've researched this and solved it semi-successfully using middleware, but my solution is erroring in certain situations.
I've implemented a refresh middleware that runs on every API action. It checks whether the access token is expired or about to expire. If so, instead of dispatching the action it received, it dispatches a token refresh action and queues up any other actions until a new access token is received. After that, it dispatches all actions in its queue.
However, one of my action creators looks something like this:
function queryThreads(params) {
return async (dispatch) => {
const threads = await dispatch(fetchThreads(params))
const newPageToken = threads.payload.nextPageToken
}
}
When the refresh middleware doesn't run because the token isn't expiring, threads.payload will be defined here and everything will work as intended.
However, when the refresh middleware does run, threads.payload will be undefined because the dispatch seems to resolve with the value of the token refresh action, rather than the fetchThreads action.
How do I ensure that the token gets refreshed (and updated in state/localStorage), fetchThreads gets dispatched with the updated token, and the threads variable gets assigned to the resolved value of the correct Promise?
Links to Project Code
This is my refresh middleware. It was inspired by this article by kmmbvnr.
This is the token refresh action creator.
This is the line in my queryThreads action creator that throws when the token has to refresh (threads.payload is undefined).
This is the reducer where I update state in response to a token refresh.
This is the middleware where I update localStorage in response to a token refresh.
It looks like I've solved the issue by rewriting the refresh middleware like this:
function createRefreshMiddleware() {
const postponedRSAAs = [];
return ({ dispatch, getState }) => {
const rsaaMiddleware = apiMiddleware({ dispatch, getState });
return next => action => {
if (isRSAA(action)) {
try {
const auth = JSON.parse(localStorage.getItem('auth'));
const { refresh_token: refreshToken } = auth;
const expirationTime = jwtDecode(auth.id_token).exp * 1000;
const isAccessTokenExpiring =
moment(expirationTime) - moment() < 300000;
if (refreshToken && isAccessTokenExpiring) {
postponedRSAAs.push(action);
if (postponedRSAAs.length === 1) {
return rsaaMiddleware(next)(
dispatch(() => attemptTokenRefresh(refreshToken))
).then(() => {
const postponedRSAA = postponedRSAAs.pop();
return dispatch(postponedRSAA);
});
}
return rsaaMiddleware(next)(action);
}
return rsaaMiddleware(next)(action);
} catch (e) {
console.log(e);
return next(action);
}
}
return next(action);
};
};
}
export default createRefreshMiddleware();
Now the postponed action will always be chained off of the token refresh action, so we don't have the problem of the original promise resolving with the wrong value; plus it's more concise.