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.
Related
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.
I'm using React Native/Firebase/Redux to build a simple login system. I am trying to work out how to capture errors that happen as a result of failed login attempts.
Here's my authscreen.js:
const [alertShowing, setAlertShowing] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
...
function handleLogin() {
const response = dispatch(login(email, password));
console.log(response);
}
actions.js:
export const login = (email, password) => {
return async (dispatch) => {
try {
const response = await Firebase.auth().signInWithEmailAndPassword(email, password);
dispatch(getUser(response.user.uid));
} catch (e) {
return e;
}
};
};
My console.log(response) above correctly shows me the error message, but this obviously isn't very useful to users. And please note too that I can log in properly when using correct credentials.
What I really want to do in my handleLogin() is check if the response is an error, and if so, setlAlertShowing(true) and setAlertMessage to what I've gotten back from the useDispatch hook so that I may display it nicely to the user.
How should I go about this? TIA.
Firebase errors messages are designed for developers and not standard users friendly. The solution is to identify authentication error code and map with user-friendly messages.
list of error code https://firebase.google.com/docs/auth/admin/errors
You can use function like this to map authError to meaningful error messages.
function mapAuthCodeToMessage(authCode) {
switch (authCode) {
case "auth/invalid-password":
return "Password provided is not corrected";
case "auth/invalid-email":
return "Email provided is invalid";
// Many more authCode mapping here...
default:
return "";
}
}
and use it later
export const login = (email, password) => {
return async (dispatch) => {
try {
const response = await Firebase.auth().signInWithEmailAndPassword(email, password);
dispatch(getUser(response.user.uid));
} catch (e) {
dispatch({type:"authError",message:mapAuthCodeToMessage(e.code)}));
}
};
};
I am working on a full stack application and I am having a bit of trouble implementing a signin action. The signin process works, but the response I am getting includes too much information. In redux the state shows the data, but then it also updates the state to include the headers and the config information (which contains the username and password of the user). Since I am saving the same data in localStorage the local storage also contains this sensitive information. Obviously this is a problem and a huge security risk so I would like to update the response to only include the data from the API. My API schema returns the following information:
res.status(200).send({
id: userInfo.id,
name: userInfo.name,
email: userInfo.email,
role: userInfo.role,
message: `Welcome Back ${userInfo.name}`,
token
})
My signin action code is as follows:
export const signin = (email, password) => async (dispatch) => {
dispatch({type: user.USER_SIGNIN_REQUEST, payload: {email, password}})
try {
const data = await axios.post("/api/users/login", {email, password})
dispatch({type: user.USER_SIGNIN_SUCCESS, payload: data})
localStorage.setItem("userInfo", JSON.stringify(data))
} catch(err) {
dispatch({
type: user.USER_SIGNIN_ERROR,
payload: err.response ?? err.response.data.message
})
}
}
my reducer code is as follows:
export const userSigninReducer = (state = {}, action) => {
switch(action.type) {
case user.USER_SIGNIN_REQUEST:
return {loading: true}
case user.USER_SIGNIN_SUCCESS:
return {loading: false, userInfo: action.payload}
case user.USER_SIGNIN_ERROR:
return {loading: false, err: action.payload}
case user.USER_SIGNOUT:
return {}
default:
return state
}
}
from the looks of this code it would appear that the reducer would set userInfo to the object that is returned from the API. This does happen but it ALSO returns the headers and the config object. I have tried to update my signin action from const data = await axios.post("/api/users/login", {email, password}) to const {data} = await axios.post("/api/users/login", {email, password}) in an attempt to deconstruct the data object from the response, however this does nothing which leads me to believe that the headers and config are being applied to state by something else. I am not sure what else I can do to troubleshoot this issue.
PS: Here is a photo of what is shown in the userInfo response in redux and localstorage. You can see the full size image by right clicking and opening in a new tab.
Okay so I took a quick walk and when I got back I just restarted my server and tried to log in again. It appears that changing the response object from my signin action from const data = await axios.post("/api/users/login", {email, password}) to const {data} = await axios.post("/api/users/login", {email, password}) did in fact fix the issue. If someone is having a similar problem I would try to deconstruct the object you want from the response.
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.
I have an action that logs in the user via email and password:
export class LoginWithCredentials {
static readonly type = '[Auth] Login With Credentials';
}
I dispatch this action whenever the login form has been submitted:
onSubmitLogin() {
this.store.dispatch(LoginWithCredentials);
}
The action handler gets from the state the email and password and call firebase.auth().signInWithEmailAndPassword, like so:
#Action(LoginWithCredentials)
loginWithCredentials({getState}: StateContext<AuthStateModel>) {
const {email, password} = getState().forms.login.model;
this.afAuth.auth.signInWithEmailAndPassword(email, password)
.catch(err => console.log(err)); // Should console.log on error
console.log('Should print something');
}
For some reason, catch is being ignored, and the console.log that should print something, is ignored too.
I tried to run this method outside of the state, and it seemed to work. Although, I want to put this logic into my action.
PS:
if I use signInWithPopup(provider) instead of signInWithEmailAndPassword, then it will work (But it's not what I need).
I think this is because you call an asynchronous function and as you are not waiting for the result then NGXS "consider the action over" (I do not know how it works behind the scene..).
If signInWithEmailAndPassword returns a promise try this :
#Action(LoginWithCredentials)
async loginWithCredentials({getState}: StateContext<AuthStateModel>) {
const {email, password} = getState().forms.login.model;
await this.afAuth.auth.signInWithEmailAndPassword(email, password)
.catch(err => console.log(err)); // Should console.log on error
console.log('Should print something');
}
Anyway as it is asynchronous you need to wait the answer (success or failure) to update your state. Something like this:
#Action(LoginWithCredentials)
async loginWithCredentials({getState, patchState}: StateContext<AuthStateModel>) {
const {email, password} = getState().forms.login.model;
const loginAnswer = await yourLoginService.login(email, password);
if( loginAnswer === 'success' ) {
patchState( { loggedIn: true };
}
}