Currently I'm messing around with JWT tokens, authentication and authorization. I'm creating a JWT token and refresh tokens. I set the refresh token in a cookie. Creating and logging in a user works as intended. However trying to refresh the token and updating it, it goes wrong.
/login
router.post("/login", passport.authenticate("local"), (req, res, next) => {
const token = getToken({
_id: req.user._id
})
const refreshToken = getRefreshToken({
_id: req.user._id
})
User.findById(req.user._id).then(
user => {
user.refreshToken.push({
refreshToken
})
user.save((err, user) => {
if (err) {
res.statusCode = 500
res.send(err)
} else {
res.cookie("refreshToken", refreshToken, COOKIE_OPTIONS)
res.send({
success: true,
token
})
}
})
},
err => next(err)
)
})
/refreshToken
router.post("/refreshToken", (req, res, next) => {
console.log("in refreshtoken")
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies
console.log(refreshToken)
if (refreshToken) {
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET)
const userId = payload._id
User.findOne({
_id: userId
}).then(
user => {
if (user) {
// Find the refresh token against the user record in database
const tokenIndex = user.refreshToken.findIndex(
item => item.refreshToken === refreshToken
)
if (tokenIndex === -1) {
res.statusCode = 401
res.send("Unauthorized1")
} else {
const token = getToken({
_id: userId
})
// If the refresh token exists, then create new one and replace it.
const newRefreshToken = getRefreshToken({
_id: userId
})
user.refreshToken[tokenIndex] = {
refreshToken: newRefreshToken
}
user.save((err, user) => {
if (err) {
res.statusCode = 500
res.send(err)
} else {
res.cookie("refreshToken", newRefreshToken, COOKIE_OPTIONS)
res.send({
success: true,
token
})
}
})
}
} else {
res.statusCode = 401
res.send("Unauthorized2")
}
},
err => next(err)
)
} catch (err) {
res.statusCode = 401
res.send("Unauthorized3")
}
} else {
res.statusCode = 403
res.send("Unauthorized4")
}
})
Cookie options
exports.COOKIE_OPTIONS = {
httpOnly: true,
secure: false,
signed: false,
maxAge: eval(process.env.REFRESH_TOKEN_EXPIRY) * 1000,
sameSite: "none",
}
App.js
function App() {
const [currentTab, setCurrentTab] = useState("login")
const [userContext, setUserContext] = useContext(UserContext)
const verifyUser = useCallback(() => {
fetch(process.env.REACT_APP_API_ENDPOINT + "users/refreshToken", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
}).then(async response => {
console.log(response)
if (response.ok) {
const data = await response.json()
console.log(data)
setUserContext(oldValues => {
return { ...oldValues, token: data.token }
})
} else {
setUserContext(oldValues => {
return { ...oldValues, token: null }
})
}
// call refreshToken every 20 seconds to renew the authentication token. Make it longer for prod.
setTimeout(verifyUser, 20000)
}).catch((error) => {
console.log(error)
})
}, [setUserContext])
useEffect(() => {
verifyUser()
}, [verifyUser])
/**
* Sync logout across tabs
*/
const syncLogout = useCallback(event => {
if (event.key === "logout") {
// If using react-router-dom, you may call history.push("/")
window.location.reload()
}
}, [])
useEffect(() => {
window.addEventListener("storage", syncLogout)
return () => {
window.removeEventListener("storage", syncLogout)
}
}, [syncLogout])
return userContext.token === null ? (
<Card elevation="1">
<Tabs id="Tabs" onChange={setCurrentTab} selectedTabId={currentTab}>
<Tab id="login" title="Login" panel={<Login />} />
<Tab id="register" title="Register" panel={<Register />} />
<Tabs.Expander />
</Tabs>
</Card>
) : userContext.token ? (
<Welcome />
) : (
<Loader />
)
}
I'm unsure what causes it that when I console.log(refreshToken) in /refreshToken it comes out as undefined. I also tried looking it up in any responses in the developer tools but couldn't find it.
Would really appreciate if anyone has some clarity about what might cause this issue.
Related
I tried to check JWT token worked actually or not. set in local storage. but after reloading, it set the original automatically. so here is any wrong or good suggestion?
front end:
in front end code is.
let userToken = localStorage.getItem("accessToken")
useEffect(() => {
axios.get(`http://localhost:5000/mycars/${user.uid}`,
{
headers: {
email: user.email,
token: userToken
}
})
.then(response => {
console.log(response);
if (response.data.success) {
setError("No car found")
}
else if (response.data.error) {
setError("Unauthorized access")
}
else {
setMycars(response.data)
setError("")
}
})
.catch(err => {
console.log("error is ", err);
})
}, [user.uid, user.email, userToken])
backend:
app.get('/mycars/:id', async (req, res) => {
const uid = req.params.id;
const getEmail = req.headers.email;
const accessToken = req.headers.token;
try {
const decoded = await jwt.verify(accessToken, process.env.DB_JWTTOKEN,
function (err, decoded) {
let email;
if (err) {
email = "invalid email"
}
if (decoded) {
email = decoded.email
}
return email;
});
// console.log(getEmail, decoded);
if (getEmail === decoded) {
const query = {}
const cursor = carCollection.find(query);
const cars = await cursor.toArray();
const mycars = await cars.filter(car => car.uid === uid)
if (mycars.length === 0) {
res.send({ success: "No car found" })
}
else {
res.send(mycars)
}
}
else {
res.send({ error: "Unauthorized access" })
}
}
catch (err) {
}
with the code , I wanted to check the JWT token manually changing in local storage. when page reloaded, the token execute new. and the edited not remaining
I have a problem with the logged in user, when I refresh the page the user gets lost. This is how I assign a JWT token:
const signToken = id => {
return jwt.sign({ id }, 'my-ultra-secure-and-ultra-long-secret', {
expiresIn: '14d',
});
};
This is how I send a token to a cookie with this function as well:
const createSendToken = (user, statusCode, res) => {
const token = signToken(user._id);
const cookieOptions = {
expires: new Date(Date.now() + 14 * 1000 * 60 * 24),
httpOnly: true,
};
res.cookie('jwt', token, cookieOptions);
// Remove password from output
user.password = undefined;
res.status(statusCode).json({
status: 'success',
token,
data: {
user,
},
});
};
This is my login controller:
exports.login = catchAsync(async (req, res, next) => {
const { email, password } = req.body;
// 1) Check if email and password exist
if (!email || !password) {
return next(new AppError('Please provide email and password!', 400));
}
// 2) Check if user exists && password is correct
const user = await User.findOne({ email }).select('+password');
if (user && (await user.correctPassword(password, user.password))) {
createSendToken(user, 200, res);
} else {
return next(new AppError('Incorrect email or password', 401));
}
});
This is my Protect controller (protect middleware):
exports.protect = catchAsync(async (req, res, next) => {
// 1) Getting token and check of it's there
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return next(
new AppError('You are not logged in! Please log in to get access.', 401)
);
}
// 2) Verification token
const decoded = await promisify(jwt.verify)(
token,
'my-ultra-secure-and-ultra-long-secret'
);
// 3) Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return next(
new AppError(
'The user belonging to this token does no longer exist.',
401
)
);
}
// 4) Check if user changed password after the token was issued
if (currentUser.changedPasswordAfter(decoded.iat)) {
return next(
new AppError('User recently changed password! Please log in again.', 401)
);
}
// GRANT ACCESS TO PROTECTED ROUTE
req.user = currentUser;
res.locals.user = currentUser;
next();
});
This is my private route with this middleware:
router.route('/:id').get(authController.isLoggedIn, postController.getPost);
The problem is when I log in I get a cookie, but I can't access the protected route (I get an error token is undefind). When I refresh the page the user is lost but the cookie remains in storage. When I try to access the protect route via postman and when I add the Authorization Bearer to the header ..... (token) I can access it.
This is my frontend user reducer:
export const userLoginReducer = (state = {}, action) => {
switch (action.type) {
case USER_LOGIN_REQUEST:
return { loading: true, isAuthenticated: false };
case USER_LOGIN_SUCCESS:
return {
loading: false,
isAuthenticated: true,
user: action.payload,
};
case USER_LOGIN_FAIL:
return { loading: false, isAuthenticated: false, error: action.payload };
case USER_LOGOUT:
return { loading: false, isAuthenticated: false, user: null };
default:
return state;
}
};
This is my user action:
export const login = (email, password) => async dispatch => {
try {
dispatch({
type: USER_LOGIN_REQUEST,
});
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const { data } = await axios.post(
'/api/v1/users/login',
{ email, password },
config
);
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
This is my login screen:
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const redirect = location.search ? location.search.split('=')[1] : '/';
const dispatch = useDispatch();
const userLogin = useSelector(state => state.userLogin);
const { loading, error, isAuthenticated } = userLogin;
console.log(isAuthenticated);
useEffect(() => {
if (isAuthenticated) {
history.push(redirect);
}
if (error) {
console.log(error);
}
}, [isAuthenticated, history, redirect, error]);
const submitHandler = e => {
e.preventDefault();
dispatch(login(email, password));
};
I stuck here about 2 days trying to complete this. Please someone help me :)
As you are setting the jwt token in a cookie, you can read it from there when making a request. You can send the cookie automatically with axios by adding withCredentials: true in your axios config object, like:
axios({
withCredentials: true
})
On the server side, you can get the jwt cookie value by looking it on the req.cookies object, like:
let token;
if (req.cookies.jwt) {
token = req.cookies.jwt;
}
I have a controllerfile where I use passport.authenticate. I declare my payload and sign my token now i need the info declared in the payload in another file so I could use them in my sql request.
Here's the code for the login auth :
login: (req, res, next) => {
console.log(" login");
passport.authenticate("local", { session: false }, (error, user) => {
console.log("executing callback auth * from authenticate for local strategy ");
//if there was an error in the verify callback related to the user data query
if (error || !user) {
next(new error_types.Error404("Email ou Mot de passe invalide !"))
}else {
console.log("*** Token generation begins ***** ");
console.log(user)
const payload = {
sub: user.id,
exp: Date.now() + parseInt(process.env.JWT_LIFETIME),
email: user.email,
name: user.prenom,
lastName: user.nom,
type:user.type,
};
const token = jwt.sign(JSON.stringify(payload), process.env.JWT_SECRET, {algorithm: process.env.JWT_ALGORITHM});
res.json({ token: token,type: user.type,userid:user.id });//added userid
}
})(req, res);
}
Now in my other file i need to get the user.id and user.type so that i could use them in my request :
const createProp=(req, res, next) => {
let con=req.con
let { xx,yy } = req.body;
con.query('INSERT INTO tab1
(xx,yy,user_id,user_type ) VALUES ($1, $2, $3, $4) ',[xx,yy,user_id,user_type],
(err, results) => {
if (err) {
console.log(err);
res.status(404).json({error: err});
}
else
{res.status(200).send(`success`)}
}
);
}
in my frontend VUEJS this is my file:
import router from '#/router'
import { notification } from 'ant-design-vue'
import JwtDecode from "jwt-decode";
import apiClient from '#/services/axios'
import * as jwt from '#/services/jwt'
const handleFinish = (values) => {
const formData = new FormData()
for (var key of Object.keys(formState)) {
formData.append(key, formState[key])//here im appending some fields in my
//form i have more than just xx,yy files i just put them as an
//example
}
const token = localStorage.getItem("accessToken");
var decoded = JwtDecode(token);
console.log(decoded)
formData.append('user_id',decoded.sub)
formData.append('user_type',decoded.type)
fileListFaisabilite.value.forEach((file) => {
formData.append('xx', file)
})
fileListEvaluation.value.forEach((file) => {
formData.append('yy', file)
})
// store.dispatch('user/PROPOSITION', formData)
}
methods:{
PROPOSITION({ commit, dispatch, rootState }, formData ) {
commit('SET_STATE', {
loading: true,
})
const proposition=
mapAuthProviders[rootState.settings.authProvider].proposition
proposition(formData)
.then(success => {
if (success) {
notification.success({
message: "Succesful ",
description: " form submited!",
})
router.push('/Accueil')
commit('SET_STATE', {
loading: false,
})
}
if (!success) {
commit('SET_STATE', {
loading: false,
})
}
})
return apiClient
.post('/proposition', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then(response => {
if (response) {
return response.data
}
return false
})
.catch(err => console.log(err))
},
},
What im looking for is how i can store in my database the userid and usertype using insertinto sql request.
You can set user data in your jwt sign function without stringify method:
const payload = {
sub: user.id,
exp: Date.now() + parseInt(process.env.JWT_LIFETIME),
email: user.email,
name: user.prenom,
lastName: user.nom,
type: user.type // <-- Add this
};
const token = jwt.sign(
payload, // Don't use JSON.stringify
process.env.JWT_SECRET,
{algorithm: process.env.JWT_ALGORITHM}
);
And access user info:
jwt.verify(token, process.env.JWT_SECRET, (err, payload) => {
if (err) {
// Handle error
}
// Get some data
let user_id = payload.sub;
let user_type = payload.type;
console.log(user_id, user_type);
next();
});
The vue file:
PROP({ commit, dispatch, rootState }, payload ) {
commit('SET_STATE', {
loading: true,
});
const prop = mapAuthProviders[rootState.settings.authProvider].prop
prop(payload)
.then(success => {
if (success) {
// success contains user information and token:
const { token, userid, type } = success;
// Save to localStorage (Optional)
localStorage.setItem("accessToken", token);
localStorage.setItem("userid", userid);
localStorage.setItem("type", type);
// This not works if don't have a JWT SEED
// var decoded = JwtDecode(token);
commit('SET_STATE', {
user_id: userid,
user_type: type,
})
//dispatch('LOAD_CURRENT_ACCOUNT')
notification.success({
message: "Succesful ",
description: " form submited!",
})
router.push('/Home')
commit('SET_STATE', {
loading: false,
})
}
if (!success) {
commit('SET_STATE', {
loading: false,
})
}
})
},
The api call file:
export async function prop(payload) {
try {
const response = await apiClient.post('/prop', payload, {
headers: { 'Content-Type': 'multipart/form-data'},
});
if (response) {
return response.data;
}
} catch (err) {
console.log(err);
}
return false;
}
I try to make authorization and permissions availlable with react-admin and a Node server:https://github.com/hagopj13/node-express-mongoose-boilerplate
For react-admin there is an exemple of code: https://marmelab.com/react-admin/Authorization.html#configuring-the-auth-provider
// in src/authProvider.js
import decodeJwt from 'jwt-decode';
export default {
login: ({ username, password }) => {
const request = new Request('https://example.com/authenticate', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
})
.then(({ token }) => {
const decodedToken = decodeJwt(token);
localStorage.setItem('token', token);
localStorage.setItem('permissions', decodedToken.permissions);
});
},
logout: () => {
localStorage.removeItem('token');
localStorage.removeItem('permissions');
return Promise.resolve();
},
checkError: error => {
// ...
},
checkAuth: () => {
return localStorage.getItem('token') ? Promise.resolve() : Promise.reject();
},
getPermissions: () => {
const role = localStorage.getItem('permissions');
return role ? Promise.resolve(role) : Promise.reject();
}
};
But i don't understand how it work and on login the server return an user object like this:
{user: {id: "5e429d562910776587c567a2", email: "admin#test.com", firstname: "Ad", lastname: "Min",…},…}
tokens: {access: {,…}, refresh: {,…}}
access: {,…}
expires: "2020-03-03T06:45:10.851Z"
token: "eyJhbGciOi..."
refresh: {,…}
expires: "2020-04-02T06:15:10.851Z"
token: "eyJhbGciOi..."
user: {id: "5e429d562910776587c567a2", email: "admin#test.com", firstname: "Ad", lastname: "Min",…}
createdAt: "2020-02-11T12:25:58.760Z"
email: "admin#test.com"
firstname: "Ad"
id: "5e429d562910776587c567a2"
lastname: "Min"
role: "admin"
updatedAt: "2020-02-11T12:25:58.760Z"
There are already tokens and role and in the server, it seems to have a permission control:
role.js
const roles = ['user', 'admin'];
const roleRights = new Map();
roleRights.set(roles[0], []);
roleRights.set(roles[1], ['getUsers', 'manageUsers']);
module.exports = {
roles,
roleRights,
};
And the auth.js
const passport = require('passport');
const httpStatus = require('http-status');
const AppError = require('../utils/AppError');
const { roleRights } = require('../config/roles');
const verifyCallback = (req, resolve, reject, requiredRights) => async (err, user, info) => {
if (err || info || !user) {
return reject(new AppError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
}
req.user = user;
if (requiredRights.length) {
const userRights = roleRights.get(user.role);
const hasRequiredRights = requiredRights.every(requiredRight => userRights.includes(requiredRight));
if (!hasRequiredRights && req.params.userId !== user.id) {
return reject(new AppError(httpStatus.FORBIDDEN, 'Forbidden'));
}
}
resolve();
};
const auth = (...requiredRights) => async (req, res, next) => {
return new Promise((resolve, reject) => {
passport.authenticate('jwt', { session: true }, verifyCallback(req, resolve, reject, requiredRights))(req, res, next);
})
.then(() => next())
.catch(err => next(err));
};
module.exports = auth;
But how to get authorization and permission works from the react-admin?
Thanks & Regards
Ludo
In react-admin there is a usePermissions() hook, which calls the authProvider.getPermissions() method on mount. This method return to you single string value like 'admin' or string array of permissions like ['admin','crm'...]. This strings are up to you how to set. In example below it's storing in localStorage, but in real life need to extract it from JWT token or retrieve it from backend.
getPermissions: () => {
const role = localStorage.getItem('permissions');
return role ? Promise.resolve(role) : Promise.reject();
}
import { usePermissions } from 'react-admin';
in function
const { permissions } = usePermissions();
return (
{ permissions == 'admin' &&
<DashboardMenuItem primaryText="Dashboard" onClick={onMenuClick} sidebarIsOpen={open} />
}
...
);
I've implemented a /api/login to login a user. I also have a page login.js, that sends the form data to the login API. My login api checks the user credentials in my database and then signs a token and saves it to a cookie.
My /api/login:
await authCollection.findOne(
{
authCode: req.body.authcode,
},
function(err, doc) {
if (err) throw err;
if (!doc) return res.status(400).send('auth not found');
if (doc) {
const token = jwt.sign({ _id: doc._id }, process.env.TOKEN_SECRET);
res
.status(200)
.cookie('token', token, {
maxAge: 2 * 60 * 60 * 1000,
httpOnly: false,
})
.send(token);
}
},
);
I can protect my api routes with passing in a middleware like this:
function(req, res, next) {
const token = req.cookies.token || '';
if (!token) {
return res.status(401).send('Access denied');
}
try {
const decrypt = jwt.verify(token, process.env.TOKEN_SECRET);
req.user = {
id: decrypt.id,
firstname: decrypt._id,
};
next();
} catch (err) {
res.status(400).send('Invalid token');
}
}
I do not understand how I can protect normal pages, which are in my /pages directory. Could somebody give me a hint in the right direction? I have already googled and read each page, but couldn't implement
It depends, what I've done is to write an hoc privateArea which returns new component that fetches users accessToken from the API, then decodes it, if user is logged in, renders the wrapped page, otherwise, renders "restricted area" message.
export function privateArea(PageComponent) {
const PrivateArea = ({ accessToken, pageProps }) => {
const user = decodeToken(accessToken);
if (user) {
return <PageComponent {...pageProps} />;
} else {
return <RestrictedArea />;
}
};
PrivateArea.getInitialProps = async (...args) => {
const accessToken = await api.getAccessToken();
if (typeof PageComponent.getInitilProps === 'function') {
const pageProps = await PageComponent.getInitilProps(...args);
}
return {
accessToken,
pageProps: pageProps || {},
};
};
return PrivateArea;
}