So I am making an application with React on the front end and express on the backend. The way my application works on the front end is I have two apps: an authenticated app and an unauthenticated app.
const {authenticated} = useAuth().authData;
return authenticated ? <AuthenticatedApp/> : <UnauthenticatedApp/>
Here the authenticated variable comes from an AuthContext that I can access globally throughout the application. What it does is store the authenticated value in localStorage. What I am stuck on is when/where I set this authenticated value. In my express backend I have a middleware that checks if the session is authenticated (the user has logged in).
app.use((req, res, next) => {
console.log('Checking session...');
if (!req.session.authenticated) {
const err = new Error('Not authenticated!');
err.status = 401;
return next(err);
} else {
return next();
}
});
I originally had a function called checkSession that would make a call to express to check if the session is authenticated. This would occur as a separate request along with every request. I see a lot of overhead in this so I am looking for an alternative. What I am considering to do now is set a header value on every response from express like so.
app.use((req, res, next) => {
console.log('Checking session...');
if (!req.session.authenticated) {
const err = new Error('Not authenticated!');
err.status = 401;
return next(err);
} else {
res.setHeader('Authenticated', true);
return next();
}
});
And then I would access this in every response on the front end and do the following.
setAuthenticated(res.header.authenticated)
Is there a simpler way to do this without making two API calls for each request or having to write setAuthenticated() in every response from the backend? What I am essentially trying to fix is the user manually changing the localStorage value for authenticated to something that isn't null.
You can update the "authenticated" state in local storage only once when the app is loaded, or even when you log the user in. If someone updates that state in the local storage it should be "their problem". Of course, your backend should separately authenticate incoming requests, based on tokens or cookies, not the value from the local storage. Should the user make a request to a secured endpoint, without valid authentication credentials (access token or cookie) then the backend should respond with a 401 response. The front end can catch 401 responses from your backend and then update the "authenticated" state in local storage, and as a result, render the unauthenticated app.
Related
I'm learning express.js and I'm stuck on one little problem.
How can I save sessions in the browser so I don't have to log in every time. I use cookie-session. I send the login data downloaded from the form for validation using the POST method to endpoint ./login. After successfully passing the validation, it saves sessions in this way: req.session.admin = 1;, and then using redirect, it redirects to the admin endpoint. After redirecting to ./admin, I try to read if there is a previously saved req.session.admin session. The above action returns me undefined, and the req.session itself returns an empty object {}. I tried console.log on the ./login endpoint and yes, everything is OK i.e. it returns 1 and on the ./admin endpoint it is undfined.
My question to you is: Has anything changed recently in the implementation of cookie-session (although I don't think so, because I do it according to the documentation available on npm), or do I need to install some package besides cookie-session?
Save the session
.post('/login', (req, res) => {
const { body } = req;
if (password === body.password && login === body.login) {
req.session.admin = 1;
res.redirect('/admin');
} else {
res.redirect('/login');
}
});
This is where my problem arises, because the session is not saved and each time the validation fails, I return to login
.all('*', (req, res, next) => {
if (!req.session.admin) {
res.redirect('/login');
return;
}
next();
})
Firstly, req.session.admin = 1 does not save session, but it assigns value to separate request. Each request is independent.
You have to return some information to client (e.g token) for further using. Token should be sent in each request in appropriate header and express should verify it.
What's more you should not send credentials in plain text.
What I recommend is to familiarise yourself with JWT concept https://jwt.io/introduction
I have a login route that if a user signs in successfully, it attaches the id of the user from the database to the req.session.user property of the request object:
router.route('/za/login')
.get(onlyGuest, (req, res) => {
res.render('login', { layout: false });
})
.post(onlyGuest, async(req, res) => {
try {
const user = await User.findByCredentials(req.body.email, req.body.password);
req.user = user;
req.session.user = req.user._id;
res.redirect('/');
} catch (err) {
console.log(err);
res.status(400).render('login', {
layout: false,
message: {
error: 'login failed!',
email: req.body.email
}
});
}
});
After this property(req.session.user) has been set, a user is directed to the home route. But there is a middleware that allows access to a certain route upon authenticated, otherwise it redirects to another page. Here is the code for the middleware:
let regex = /^\/za(\/.*)?$/; /*a route prefixed with '/za', followed by zero or /anything_else*/
app.use(async (req, res, next) => {
let url = req.originalUrl;
if(!req.session.user && !regex.test(url)) {
console.log('You are trying to access an authenticated route. Login first!');
console.log(req.session.user);
if(url === '/') {
res.redirect('/za');
return;
}
res.redirect('/za/login?forbidden');
return;
}
/*req.session.user property is true or has a value set*/
if (req.session.user) {
try {
const user = await User.findById(req.session.user);
if(!user) {
return res.redirect('/za/login?forbidden');
}
req.user = user;
return next();
} catch (err) {
console.log(err);
return res.render('errors/500');
}
}
});
After successfully logging in via the login route(providing the correct credentials), i wonder why the request.session.user is still undefined and therefore redirecting me to a page accessible by unauthenticated users.
I have a feeling that i am missing the timing of order of execution. But this is just a feeling.Actually i don't know where i may have errored.
Short answer: You have to set the session or token or whatever you use for authentication on the client side.
Long answer:
You have the req object and the res object available in your handlers. The req object is the request and data sent by the client (web browser). The res object is what is sent back to the client as a response. As soon, as you call any terminating method on the res object like res.render or res.send, the handling of the specific client request is done and the linked objects are "thrown away". Therefore, setting any value on the req object does never affect the client, as the req object never gets sent back to the client. Modifying the req object would only be of use if you want to access some values on the req object in another middleware dealing with the same request.
So, you have to send the user id in the response object back to the client upon successful authentication and on the client side, take this id and set it e.g. as permanent header to be sent along with every request sent from the client from now on.
Hint: This way of authentication is not very secure. Usually, you would generate a token - e.g. a JSON Web Token (JWT-token) on the server-side upon successful authentication, send this token to the client and verify this token on each subsequent request.
Hint 2: Search (on Stackoverflow) for e.g. "node.js authentication" to get more input.
I am using the following code to generate a JWT token:
jwt.sign(id, TOKEN_SECRET, { expiresIn: '24h' });
Once generated, I send the token to the client, which stores it within a cookie:
document.cookie = `session=${token}` + ';' + expires + ';path=/'
Furthermore, I am using vue.js Router for my navigation. From my understanding, if one adds the following code in the router file, one can insert middle-ware in order to protect some routes.
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
let token = Vue.cookie.get('session')
if (token == null) {
next({
path: '/',
params: { nextUrl: to.fullPath }
})
}
} else {
next()
}
})
However, I am having difficulty understanding how can one verify the validity of the JWT token using this approach, which needs to be done on the server, where the TOKEN_SECRET is stored, and not on the client side.
Let me start with this: your goal in guarding routes is to prevent the user from having a bad experience by proceeding to a page that will attempt to retrieve information that they are not authorized to view.
So, you don't need to validate the token on the client side. Since a token will only be in hand if the server validated the user and returned a token, you - the author of the client code - can use the presence of the token as a means to inform what route to take the user through.
In other words, the client having a token is all the validation you need to allow the user through to protected routes.
Remember, it is not as though a protected page has private data in and of itself. A protected page will always retrieve that protected data from the server, which means that the server has the chance to authenticate the token after all.
I am building my application using Angular and node.js (express) and I am using JWT for authentication and authorization. Currently, I am storing JWT in session object on server in memory (req.session.jwt = user.generateToken()) and also, I am sending refresh token in HttpOnly cookie to the client.
When JWT expires, I want server to give me new JWT, if refresh token from cookie is equal to the refresh token in database - asociated with user.
I tried to implement refresh token logic in my auth middleware in the catch block, but it did not work.
(If token validation failed, then compare RF token from cookie to RF in database, if true, set new JWT and run the routehandler.)
module.exports = function(req, res, next) {
const token = req.session.jwt;
if (!token) return res.status(401).send("Access denied.");
try {
const decoded = jwt.verify(token, config.get("jwtPrivateKey"));
req.user = decoded;
next();
} catch (ex) {
(async () => {
let user = await User.findOne({ username: req.session.username });
if (!user) return res.status(500).send("Invalid session");
if (user.refreshToken === req.signedCookies.refreshToken) {
req.session.jwt = user.generateToken();
next();
} else {
return res.status(401).send("Invalid session");
}
})();
}
};
I had implemented this refresh token logic, when I was storing token and refresh token in localstorage on client, but this was not great implementation, so I tried to make it like this.
I had token.interceptor in my Angular part of the project - from this tutorial (https://angular-academy.com/angular-jwt/#refresh-token)
Here is my github repo of the backend https://github.com/TenPetr/dashboard_backend
If you have any idea, how to implement this logic, please let me know.
Thanks for your advices.
When you generate a new JWT token, as the request is going to continue to next middleware step, probably you will need to set the user, same as you do when the jwt is correct.
Something like this, try it, and let me know if it helps:
if (user.refreshToken === req.signedCookies.refreshToken) {
req.session.jwt = user.generateToken();
const decoded = jwt.verify(req.session.jwt, config.get("jwtPrivateKey"));
req.user = decoded;
next();
}
I'm developing a web application using Node.js/Express.js for the backend and I use Firebase for user authentication, and to manage user registration etc I use Firebase Admin SDK.
When a user want to login I sign him in using Firebase Client SDK like this:
// Handling User SignIn
$('#signin').on('click', function(e){
e.preventDefault();
let form = $('#signin-form'),
email = form.find('#email').val(),
pass = form.find('#password').val(),
errorWrapper = form.find('.error-wrapper');
if(email && pass){
firebase.auth().signInWithEmailAndPassword(email, pass)
.catch(err => {
showError(errorWrapper, err.code)
});
}else {
showError(errorWrapper, 'auth/required');
}
});
Below this code, I set an observer to watch for when the user successfully sign in, After a successfull sign in I get a Firebase ID token which I send to an endpoint on the server to exchange it for a session cookie that has the same claims the ID token since the later expires after 1 hour.
// POST to session login endpoint.
let postIdTokenToSessionLogin = function(url, idToken, csrfToken) {
return $.ajax({
type: 'POST',
url: url,
data: {
idToken: idToken,
csrfToken: csrfToken
},
contentType: 'application/x-www-form-urlencoded'
});
};
// Handling SignedIn Users
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
user.getIdToken().then(function(idToken) {
let csrfToken = getCookie('csrfToken');
return postIdTokenToSessionLogin('/auth/signin', idToken, csrfToken)
.then(() => {
location.href = '/dashboard';
}).catch(err => {
location.href = '/signin';
});
});
});
} else {
// No user is signed in.
}
});
Sign in endpoint on the server looks like this:
// Session signin endpoint.
router.post('/auth/signin', (req, res) => {
// Omitted Code...
firebase.auth().verifyIdToken(idToken).then(decodedClaims => {
return firebase.auth().createSessionCookie(idToken, {
expiresIn
});
}).then(sessionCookie => {
// Omitted Code...
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({
status: 'success'
}));
}).catch(err => {
res.status(401).send('UNAUTHORIZED REQUEST!');
});
});
I have created a middle ware to verify user session cookie before giving him access to protected content that looks like this:
function isAuthenticated(auth) {
return (req, res, next) => {
let sessionCookie = req.cookies.session || '';
firebase.auth().verifySessionCookie(sessionCookie, true).then(decodedClaims => {
if (auth) {
return res.redirect('/dashboard')
} else {
res.locals.user = decodedClaims;
next();
}
}).catch(err => {
if (auth) next();
else return res.redirect('/signin')
});
}
}
To show user information on the view I set the decoded claims on res.locals.user variable and pass it to the next middle ware where I render the view and passing that variable like this.
router.get('/', (req, res) => {
res.render('dashboard/settings', {
user: res.locals.user
});
});
So far everything is fine, now the problem comes after the user go to his dashboard to change his information (name and email), when he submits the form that has his name and email to an endpoint on the server I update his credentials using Firebase Admin SDK
// Handling User Profile Update
function settingsRouter(req, res) {
// Validate User Information ...
// Update User Info
let displayName = req.body.fullName,
email = req.body.email
let userRecord = {
email,
displayName
}
return updateUser(res.locals.user.sub, userRecord).then(userRecord => {
res.locals.user = userRecord;
return res.render('dashboard/settings', {
user: res.locals.user
});
}).catch(err => {
return res.status(422).render('dashboard/settings', {
user: res.locals.user
});
});
}
Now the view gets updated when the user submits the form because I set the res.locals.user variable to the new userRecord but once he refreshes the page the view shows the old credentials because before any get request for a protected content the middle ware isAuthenticated gets executed and the later gets user information from the session cookie which contains the old user credentials before he updated them.
So far these are the conclusions that I came to and what I tried to do:
If I want the view to render properly I should sign out and sign in again to get a new Firebase ID token to create a new session cookie which is not an option.
I tried to refresh the session cookie by creating a new ID token from the Admin SDK but it doesn't seem to have this option available and I can't do that through the client SDK because the user is already signed in.
Storing the ID token to use later in creating session cookies is not an option as they expire after 1 hour.
I Googled the hell out of this problem before posting here so any help is so much appreciated.
I am facing a very similar scenario with one of my apps. I think the answer lies in these clues.
From Firebase docs
Firebase Auth provides server-side session cookie management for traditional websites that rely on session cookies. This solution has several advantages over client-side short-lived ID tokens, which may require a redirect mechanism each time to update the session cookie on expiration:
So they're hinting here that you want to manage the session and it's lifetime from the server.
Second clue is in the docs
Assuming an application is using httpOnly server side cookies, sign in a user on the login page using the client SDKs. A Firebase ID token is generated, and the ID token is then sent via HTTP POST to a session login endpoint where, using the Admin SDK, a session cookie is generated. On success, the state should be cleared from the client side storage.
If you look at the example code, the even explicitly set persistence to None to clear state from the client using firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
So they are intending there to be no state on the client beyond the initial auth. They explicitly clear that state and expect an httponly cookie so the client can't grab the cookie (which really is just the ID token) and use it to get a new one.
It is odd that there is no clear way of refreshing the token client-side but there it is. You can only really create a session cookie with a super long lifetime and decide on the server when to delete the cookie or revoke the refresh token etc.
So that leaves the other option: manage state client-side. Some examples and tutorials simply send the ID token from the client to the server in a cookie. The satte sits on the client and the client can use the ID token to use all firebase features. The server can verify the user identity and use the token etc.
This scenario should work better. If the server needs to kick the user then it can delete the cookie revoke the refresh token (a bit harsh admittedly).
Hope that helps. Another scheme would be to build custom tokens, then you have complete control.