I have a sails.js app that generates API to my client. In order to secure my API I need to implement OAuth2.0 to my sails app. I have started to follow this tutorial: https://www.npmjs.com/package/sails-generate-auth#requirements
But I get all kinds of diffrent errors when every time when I try to lift the server. I also dont understand to where i'm suppose to send my credentials to the server and get the access token. I'm fairly new to Sails.js and just got to know OAuth and I can't find a proper guide on how to implement OAuth.
How can I implement OAuth to my app? please have a detailed answer so that I can fully understand.
UPDATE:
ok so instead I started to follow this guide: https://www.bearfruit.org/2014/07/21/tutorial-easy-authentication-for-sails-js-apps/
and I think I got everything to work as it should(?) when I register an account it saves the data in the database as it should. The login also seems to work properly But I didn't understood how I can access the actuall data like the username and email address after the login redirects me to the homepage? I've tested the login on postman and when i log in I get a cookie. What am I suppose to do with it?
The AuthController generated by sails-generate-auth doesn't add the user details to the session by default so you should add it manually by adding the following line to the callback function in AuthController.js
req.session.user = user;
This is how the callback looks like with the line:
callback: function (req, res) {
function tryAgain (err) {
// Only certain error messages are returned via req.flash('error', someError)
// because we shouldn't expose internal authorization errors to the user.
// We do return a generic error and the original request body.
var flashError = req.flash('error')[0];
if (err && !flashError ) {
req.flash('error', 'Error.Passport.Generic');
} else if (flashError) {
req.flash('error', flashError);
}
req.flash('form', req.body);
// If an error was thrown, redirect the user to the
// login, register or disconnect action initiator view.
// These views should take care of rendering the error messages.
var action = req.param('action');
switch (action) {
case 'register':
res.redirect('/register');
break;
case 'disconnect':
res.redirect('back');
break;
default:
res.redirect('/login');
}
}
passport.callback(req, res, function (err, user, challenges, statuses) {
if (err || !user) {
return tryAgain(challenges);
}
req.login(user, function (err) {
if (err) {
return tryAgain(err);
}
// Mark the session as authenticated to work with default Sails sessionAuth.js policy
req.session.authenticated = true;
req.session.user = user;
// Upon successful login, send the user to the homepage were req.user
// will be available.
res.redirect('/');
});
});
}
You can now use the user details in any of your controllers and views by referring to req.session.user for example twitter provides your user name so you can use req.session.user.username.
Related
I'm currently working on a MEAN full stack web project for a little marketplace app. This project's build in 3 parts :
- server -> node.js express mongoose
- front web -> angular 4
- front mobile -> ionic
I've to build this simple API REST, with a classic CRUD, but I have to use JWT to secure my adverts (for the market place).
An user will be able to delete or modify only the advertisements which he himself create, using JWT verification.
Currently, I have a token verification's middle-ware, but it does not prevent a user from deleting an ad created by another user.
I'm calling my middle-ware as I understood on tutorials, it can be change.
And after a lot of research, i only found information about authentication with JWT, then if someone can help my, thanks.
//my token verification's middle-ware
function verifyToken(req, res, next) {
var token = req.headers['x-access-token'];
if (!token)
return res.status(403).send({ auth: false, message: 'No token provided.' });
jwt.verify(token, config.secret, function(err, decoded) {
if (err)
return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });
// if everything good, save to request for use in other routes
req.userId = decoded.id;
next();
});
}
//an example of middle-ware call
router.delete('/:id',VerifyToken, (req, res) => {
advertModel.findById(req.params.id, (err, advert) => {
if(!advert){
res.json({message:"No advert corresponding"})
}
advert.remove((err) => {
if(err){
console.log(err);
}
res.json({message: 'Successfully deleted'});
});
});
});
This application is still under construction, then if you have any comments that would allow me to improve the few pieces of code that you see, go.
jwt token when comes with a request, if that token is valid it just pass the request to next with that user credential, but if the token is not valid, it stops the request lifecycle. But it does not have anything to do with the things you are trying to do.
You can write a simple conditional in your controller code like this
if(req.user.id !== youradd.user_id){
return ('with valid message and http code')
#shahinmahmud is right. Basically there are two parts to what you are doing. Authentication and Authorization. Authentication is done by JWT token validation. Authorisation is to restrict access based on the user. In your case, if it's just access to one resource, a simple if-else will do. Otherwise you need to look into some user management libraries.
This definition should probably help
I have a stupid question with feathersjs auth hooks (or whatever). This is my code with comment:
app.get('/admin', function (req, res) {
// i'd like to check here if (req.connection.isAdmin) then render page
res.render('admin/admin.html', {
title: 'admin'
})
});
I can't find where i can implement user-auth hook to chek user for admin role. How can i do that?
You should be able to use the example that I posted in this other question: Feathers Js Restrict Access To Page on Server Side
In your case, you'll need to do some logic to see if the user is an administrator. By default, the JWT token that Feathers gives you will only contain the userId. You can retrieve the user record to check if the user is an administrator.
Here's an example of what you would put inside the jwt.verify block:
jwt.verify(token, secret, function(err, decoded) {
if (err) {
return res.status(401).send('You are not authorized to view that page.');
}
app.service('users')
.get(decoded.id) // Or _id if you're using MongoDB
.then(user => {
if (user.isAdmin) {
res.render('admin/admin.html', {
title: 'admin'
})
} else {
return res.status(401).send('You are not authorized to view that page.');
}
})
});
It will be possible in the next version of feathers-authentication to add values to the JWT on the server, so, for administrators, you could add an isAdmin property to the JWT payload. This will be fairly trivial to do once the pre-release version is published. Until then, the above is probably the best way to go.
I'm trying to use this library to authenticate using Linkedin:
https://github.com/auth0/passport-linkedin-oauth2
No Linkedin Login Prompt
I have configured my Passport Linkedin Strategy like so:
var passport = require('passport');
var LinkedInStrategy = require('passport-linkedin-oauth2').Strategy;
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function (err, user) {
done(err, user);
});
});
passport.use(new LinkedInStrategy({
clientID: 'LINKEDIN_API_KEY',
clientSecret: 'LINKEDIN_API_SECRET',
callbackURL: 'http://localhost:1337/auth/linkedin/callback',
scope: ['r_emailaddress', 'r_basicprofile'],
state: true
}, function(accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
// To keep the example simple, the user's LinkedIn profile is returned to
// represent the logged-in user. In a typical application, you would want
// to associate the LinkedIn account with a user record in your database,
// and return that user instead.
return done(null, profile);
});
}));
My AuthController.js looks like this:
var passport = require('passport');
module.exports = {
login: function(req, res) {
passport.authenticate('linkedin', function(err, user, info) {
// The request will be redirected to LinkedIn for authentication, so this
// function will not be called.
});
},
callback: function(req, res) {
// ------------------------------------------------------------------------
// after user authenticated, we get the user's email from
// Linkedin's JSON response and save it against the matching
// email address in the User model
// ------------------------------------------------------------------------
console.log(res);
},
logout: function(req, res) {
req.logout();
res.send('logout successful');
}
};
From the linkedin oauth library, I expect the call to:
passport.authenticate('linkedin', function...);
In my AuthController's login action, to redirect the user to Linkedin's login prompt page but what I am actually seeing is my browser just keeps on loading, loading, loading and never stops.
Am I doing something wrong ?
Some questions I am not sure of:
Does Linkedin expect my server to be running on HTTPS before it lets this whole thing starts working ?
Is there some special configurations that I need to do in my Linkedin developers app setting ? (I've enabled all the correct Javascript SDK URLs)
Callback Error
OK, so continuing on, my next problem appears to be here:
return done(null, profile);
^
TypeError: object is not a function
My code is following the npm module instruction here: https://www.npmjs.com/package/passport-linkedin-oauth2
Maybe SailsJS has another way of writing it yet again....
Authentication Always Fails
After fixing the callback error as mentioned in my solution below, I decided to keep moving on and see how it goes despite the Linkedin documentation isn't quite matching 100% to what I expect from the NPM library.
My next problem is my authenticated.js policy appears to always fail.
My code is below:
// We use passport to determine if we're authenticated
module.exports = function (req, res, next) {
if(req.authenticated) { // <---- this is the error line
return next();
}
else
{
res.send(401, {
error: 'Nice try buddy. Try logging in with Linkedin first :]'
});
}
};
No Login Prompt Solution
sigh
I think I'm beginning to grasp some of the difference between SailsJS and pure ExpressJS codes.
The problem appears that I was missing this piece of code at the end of my passport.authenticate() method:
(req, res)
I picked it up after looking this tutorial again: http://iliketomatoes.com/implement-passport-js-authentication-with-sails-js-0-10-2/
So now, the final authenticate method should look like:
passport.authenticate('linkedin', function(err, user, info) {
// The request will be redirected to LinkedIn for authentication, so this
// function will not be called.
})(req, res); // <--- notice this extra (req, res) code here
Which matches the Passportjs documentation:
passport.authenticate('local'),
function(req, res) {
// If this function gets called, authentication was successful.
// `req.user` contains the authenticated user.
res.redirect('/users/' + req.user.username);
});
In a way....if you know what I mean... :D
Now I got my Linkedin login prompt as expected.
Finally!
Callback Error Solution
OK.....I'm not sure if this is completes the login process...but....
I noticed I had an extra line:
passReqToCallback: true
Taken from this page here:
https://github.com/auth0/passport-linkedin-oauth2/issues/29
I removed that and I got a different error message.
I've also changed my callback code to look like:
passport.authenticate('linkedin', function(err, user, info) {
res.json(200, {
user: user
});
})(req, res);
and I got my user JSON which appears to be my Linkedin user profile info:
{
user: {
provider: "linkedin",
...
}
}
But that's...contradicting the Linkedin documentation...I don't see any access_token or expire_in properties which I was expecting to see in step 3 of the Linkedin OAuth 2.0 documentation (https://developer.linkedin.com/docs/oauth2)...
So...supposedly...I should take this user object and create/update against an existing user object ?
Authentication Always Fails Solution
OK, so few more days, I added extra code to generate a User entity if one isn't found in my database, otherwise just return the found user.
The was one last problem, in my policies folder, I have a authenticated.js and it looked like this:
// We use passport to determine if we're authenticated
module.exports = function (req, res, next) {
if(req.authenticated) { // <---- this is the error line
return next();
}
else
{
res.send(401, {
error: 'Nice try buddy. Try logging in with Linkedin first :]'
});
}
};
Being new to all this web development stuff, I thought:
req.authenticated; // should call match name of the file ?
was correct but I was following this tutorial:
http://iliketomatoes.com/implement-passport-js-authentication-with-sails-js-0-10-2/
and he named his file: isAuthenticated.js I figured it's just a name....but I was wrong :D
Turns out, the correct code was:
req.isAuthenticated()
So in full, the correct code becomes:
// We use passport to determine if we're authenticated
module.exports = function (req, res, next) {
if(req.isAuthenticated()) { // alright, that's more like it!
return next();
}
else
{
res.send(401, {
error: 'Nice try buddy. Try logging in with Linkedin first :]'
});
}
};
Perhaps isAuthenticated is a Passportjs function and not just a name like I initially thought.
My further research shows this page which suggests so to me:
Problems getting Passport.js to authenticate user
Maybe req.authenticated can only be used for HTML email-password login form as suggested in above Stackoverflow post and req.isAuthenticated() is for OAuth stuff.
Anyhow, I still don't know if this is the right path but so far, I got authentication in my application now and I can access protected resources. Not sure how long I'll be logged in for, maybe I still need to build the refresh token thingo every 15 minute like the Linkedin documentation stated ?
Hope this helps other fellow Sailsjs users who are facing the same problem :)
Does Linkedin expect my server to be running on HTTPS before it lets
this whole thing starts working ?
No. The API works just as well on a local http setup.
Is there some special configurations that I need to do in my Linkedin
developers app setting ? (I've enabled all the correct Javascript SDK
URLs)
No, your setup is fine.
The browser keeps loading because after the authentication LinkedIn redirects to your callback action which isn't handling the response stream.
You need to handle the response in the callback action. Something like this will do:
callback: function(req, res) {
passport.authenticate('linkedin', function(err, user){
// handle error
// do something with the user (register/login)
return res.redirect('/home');
});
}
I'd highly recommend using sails-generate-auth for maintaining third-party logins. Very easy to setup and configure. All you need to do is serve the access tokens and secrets for the different strategies (either through config/passport.js or, preferably, through config/local.js). Will spare you a lot of redundant code.
I am in search of a way to redirect a user to the previous page he or she was on after successful login. For instance, if a user wants to use a feature only accessible to members, he or she would be redirected to a login page, and on a successful login, would then be redirected to what ever page the user was trying to access before being logged in. Here is what I have so far:
Middleware:
function requireLogin(req, res, next){
if (!req.user) res.redirect('/login');
else next();
}
Login:
app.post('/login', function(req,res){
User.findOne({email: req.body.email}, function(err, user){
if (!user) res.render('login.jade',{error: 'Invalid email or password.'});
else{
if (bcrypt.compareSync(req.body.password,user.password)) {
req.session.user = user; //set cookie session
res.redirect('/forums');
}
else res.render('login.jade',{error: 'Invalid email or password.'});
}
});
});
Example route:
app.post('/article/comment', requireLogin, function(req,res){...
I have seen a few SO posts about this subject, but no really qualifying answers(especially without PassportJS). Suggestions?
I was able to solve this issue with a few simple additions to my existing code:
function requireLogin(req, res, next){
if (!req.user) {
req.session.prevUrl = req.body.url;
if(req.xhr) res.send({"err":"usrErr"});
else res.redirect('/login');
}
else next();
}
app.post('/login', function(req,res){
User.findOne({email: req.body.email}, function(err, user){
if (!user) res.render('login.jade',{error: 'Invalid email or password.'});
else{
if (bcrypt.compareSync(req.body.password,user.password)) {
req.session.user = user; //set cookie session
if(req.session.prevUrl) res.redirect(req.session.prevUrl);
else res.redirect('/forums');
}
else res.render('login.jade',{error: 'Invalid email or password.'});
}
});
});
I saved the page a user was on when trying to access a feature only available to members as session data. Note, this is a much better solution than the 'back' solution, as (for instance if the user inputs the wrong login information, the previous page will not now be the login page, but still the page saved in the session). With the session storage now saved, a simple check in the login can be done to see if the saved session url exists. If it does, redirect to the saved session url, else redirect to the default home.
P.S. The line, if(req.xhr) res.send({"err:"usrErr"}); is a check to see if the request made was a JQuery request. This is because a redirect will not be performed if the request was made through JQuery. The err response allowed me to do a simple check in my JQuery function to perform a redirect if the user did not exist. Note, the session data is still saved in this case so, simply add
window.location.href='/login'; to your function in order to perform the redirect within the script.
Thanks to #mikeyhew for pointing me in the right direction.
You can use res.redirect('back'), the only caveat is it relies upon the http-referer header which is not reliable, when the http-referer header is omitted it defaults to /. Check the docs.
It would help if you stored the request url from your requireLogin middleware for future use:
function requireLogin(req, res, next){
if (!req.user) {
var postLoginRedirectUrl = req.originalUrl;
// store this url somehow (session or query params?)
res.redirect('/login');
} else {
next();
}
}
I've seen some sites use query params; you just have to make sure the url doesn't get lost in the login process.
Then when the user logs in successfully, just redirect them to that url.
I am trying to build a RESTful API using Node.js w/ Express. I am fairly new to the MEAN stack, and want to use best practices. The concept I'm having trouble grasping and implementing is the following:
Restricting routes like PUT and DELETE on a user object, to only allow requests from users who 'own' this object.
One possibility I've thought of:
Creating secret token for users that matches token in DB
So when creating a user I assign them a token, store this in the DB and attach it to their session data.
Then my middleware would look something like:
router.put('/api/users/:user_id', function(req, res, next) {
// already unclear how this token should be transfered
var token = req.headers['x-access-token'] || req.session.token;
// update user (PUT /api/users/:user_id)
User.findById(req.params.user_id, function(err, user) {
if (err) {
res.send(err);
} else if (user.token != token) {
res.json({ sucess: false, message: 'User not same as authenticated user.' });
} else {
// set new information only if present in request
if (req.body.name) user.name = req.body.name;
if (req.body.username) user.username = req.body.username;
...
// save user
user.save(function(err) {
if (err) res.send(err);
// return message
res.json({ message: 'User updated.' });
});
}
});
Questions I have regarding best practice
Is the scenario I thought of at all plausible?
What data should I use to create a unique token for a user?
Is storing the token in the session the best solution?
Sidenote
This is a learning project for me, and I am aware of libraries like Passport.js. I want to learn the fundamentals first.
I have a repo for this project if you need to see some of the surrounding code I'm using: https://github.com/messerli90/node-api-ownership
Edit
I would accept a good RESTful API book recommendation, where these points are covered, as an answer.
Edit 2
I actually found a lot of the answers I was looking for in this tutorial: http://scottksmith.com/blog/2014/05/29/beer-locker-building-a-restful-api-with-node-passport/
I was trying to do this without the use of passport.js but a lot of the concepts covered in the article made some of the mechanics of an authorized API clear to me.
If I understand your question, this is an API, and the client (not a browser) is passing the secret token (api key) in the request, in a header. Seems reasonable. Of course, you must require https to protect the api key. And, you should have a way for users to revoke/regenerate their API key.
So far, I don't think you need to store anything in the session. It seems like storing the token in the session just complicates things. Presumably, if you are going to establish a session, the client has to include the token in the first request. So, why not just require it on each request and forget the session? I think this makes life simpler for the api client.
A 'bit' too late, but if someone is still looking for an answer, here is how i did it:
router.put('/', function(req, res) {
var token = req.headers['x-access-token'];
if (!token) return res.status(401).send({auth:false, message:'No token provided'});
jwt.verify (token, process.env.SECRET, function (err, decoded) {
if(err) return res.status(500).send({auth:false, message:'failed to auth token'});
User.findByIdAndUpdate({_id: decoded.user_id}, req.body, function(err, user) {
if (err)
res.send(err);
res.json({username: user.username, email: user.email});
});
});
});
Just pass the user id that is stored in the token to the mongoose function. This way the user who sent the request can only update or delete the model with his ID.
Reading material:
Implementing Access Control in Node.JS
Found this super clear article on how to allow users to only delete replies they own. Hope it helps.
What worked for me:
.delete(requireAuth, async (req, res, next) => {
const knexInstance = req.app.get("db");
const comment = await CommentsService.getById(knexInstance, req.params.id);
if (comment === undefined) {
return res.status(404).json({
error: {
message: `Comment doesn't exist.`
},
});
}
if (comment.users_id !== req.users.id) {
return res.status(401).json({
error: {
message: `You can only delete your own comments.`
},
});
}
CommentsService.deleteComment(knexInstance, req.params.id)
.then((numRowsAffected) => {
res.status(204).end();
})
.catch(next);
})