I have a REST endpoint that is being submitted with a JSON object (User) and I just set the corresponding mongo record to that JSON object. This saves me the trouble of updating schema changes in the service method and the endpoint leaving just the Mongoose model to update.
What would be a more secure way of doing this, if any?
Example User JSON
{
'fname': 'Bill',
'lname': 'Williams',
'email': 'bill#billwilliams.com',
'settings': {
'strokeColor': '#FF0000'
}
}
From my Angular service
Update: function(my_user) {
return $http.put('http://api.domain.com/v1/api/users/' + _user.id, {
user: my_user,
token: window.localStorage['token']
});
}
My REST endpoint in Node
api.route('/users/:user_id')
.put(function(req, res) {
User.findById(req.params.user_id, function(err, user) {
userData = req.body.user;
if (user) {
//-- This is potential trouble area?
User.update({'_id': user._id}, {$set: userData});
user.save(function(err) {
res.json({
success: true,
message: 'User updated'
});
}); //-- end findById()
}); //-- end /users/:user_id put() route
Have a look at Jsonwebtoken.
It basically works like this:
Create a REST endpoint that lets users aquire a token with a certain
payload (id for example)
Secure the relevant part of your api with the Jsonwebtoken middleware (if using express as the webserver)
User adds the token to every request header (by using $httpInterceptor)
Token is checked on the server side before the request reaches your API
Tokens may expire after a certain time (useful when users needs to register first) which adds additional security.
Related
i was wandering what is the best way to avoid sending the user data on every request,
lets say i want to add product from user's account, i have to send the user. or i want to order something, i have to send the user.
i thought about something like this:
app.use(async (req, res, next) => {
if (!req.body.userId) {
return next();
}
const user = await enteties.User.findByPk(req.body.userId);
req.user = user;
next();
});
but it also requires me to send the user on evey request..
there must be a way to avoid sending the user data to the server on almost every request.
also, it will make all my requests of type "post" since i have to send the user, and even "get" requests are now become "posts", for sure this is not correct
If you implement your JWT token correctly you don't need to send the logged in user id.
JWT tokens contain a payload section that is basically any JSON data you want to set. This is basically your decentralized session stored in the user's machine. When creating a JWT token you'd normally do something like:
const jwt = require('jsonwebtoken');
const config = require('./config');
function generateToken(user) {
let payload = {
sub: user.id
};
return jwt.sign(payload, config.secret, {
algorithm: 'HS512', // choose algorithm appropriate for you
expiresIn: config.expires
})
}
That payload part allows you to send user identifying information. In the case above, the user id. To get that id from a request simply verify it:
app.use((req, res, next) => {
const token = req.get('Authorization');
jwt.verify(token, config.secret, (err, payload) => {
if (err) {
next(err);
}
else {
req.user = payload; // user.sub is the user id
next();
}
});
});
Or you can use a library such as express-jwt to do it for you:
const expressJwt = require('express-jwt');
const express = require('express');
const config = require('./config');
const app = express();
app.use(expressJwt({ secret: config.secret }); // use express-jwt like any
// middleware, you can even install
// it on specific routes.
Now in your controller/route you can simply extract the payload in the req.user object. Invalid tokens or requests without tokens will completely skip your handler and immediately return an error or unauthorized response:
app.get('/some/endpoint', (req, res) => {
console.log('user is', req.user.sub); // note: req.user is our payload
});
Additional tricks:
As I mentioned, the payload is basically user defined. If you need to keep track of other user information such as roles or permissions you can store them in the JWT token:
// Example payload
let payload = {
sub: user.id,
admin: user.role === 'admin',
gender: user.gender
};
This reduces the number of database requests needed to process the user. Making the authentication system completely decentralized. For example you may have a service that consumes this JWT token that is not connected to your user database but need to check if user is admin. With the right payload that service does not even need to have access to the user database.
Note however that the payload is not encrypted. It is just base64 encoded. This means that the information in the token can be easily read by anyone with access to it (normally the user but beware of 3rd party scripts). So ideally you shouldn't store dox-able information in the payload if you have 3rd party scripts on your website (then again, it is highly unusual these days for anyone to write the entire front-end from scratch without any libraries or frameworks)
Also note that the more you put in your payload the larger your token will be.
As shown in the illustration below, I have a standalone API Project running on a server with a port say 3001, and I have a Web App running on a server with port say 3002.
API on port 3001, has all the API routes required for the Web App (& mobile apps) to fetch and put data, including Authentication API (Using passport-local and passport-jwt). In the API side of the project, I have also handled user role authorization, and every routes has list of roles who can access the APIs.
Example Route
todoRoutes.get('/',
requireAuth,
AuthController.roleAuth(['user','editor','admin']),
TodoController.getTodos);
Role Authorization API Method in port 3001
exports.roleAuth = function(roles){
return function(req, res, next){
var user = req.user;
User.findById(user._id, function(err, foundUser){
if(err){
res.status(422).json({error: 'No user found.'});
return next(err);
}
if(roles.indexOf(foundUser.role) > -1){
return next();
}
res.status(401).json({error: 'You are not authorized to view this content'});
return next('Unauthorized');
});
}
}
Response json after login successfully is like this
{
"token": "JWT eyJhbGci...",
"user": {
"_id": "5986b81d940bab06ddc79b34",
"email": "myemail#gmail.com",
"role": "admin"
}
}
Now in Web App, I want to use same role authorization and authentication (login), but you see, Web App is not connected to database, for me to make queries like check if the user in session is valid and has the role as in the response it got after login successfully.
Summary
Here are bullet points of what I was looking for in this question:
Login on Client-Side Web Application, via Remote API on port 3001 (achieved)
Get User Token and other information (response shown above) (achieved)
Ensure user is authenticated on Client-Side Web App and also remember role of the user loggedin, to use these information for authorization of every routes on Client-side app. In client-side app I have few pages with forms to send data to Server-Side API on port 3002, these pages are used by two different user with roles editor and admin.
TIA
Your authenticating API should return a JWT with guaranteed information (the role) embedded. Further, the token should be made using a secret known to your view APIs.
For example, using npm module jsonwebtoken, sign it like so:
token = jwt.sign( {
exp: Math.floor( Date.now() / 1000 ) + ( 60 * 60 ), // 1 hour
i: user._id,
role: user.role
}, "my-secret" );
Then, on your view API, use passport-jwt, which both verifies the token and provides you a payload that matches the original object you signed. Use the payload as the user object:
passport.use( new JwtStrategy( {
secretOrKey: "my-secret"
}, ( payload, callback ) => callback( null, payload ) ) );
const authenticate = () =>
passport.authenticate( "jwt", { session: false, failWithError: true } )
At this stage, your user is at minimum authenticated. If you want to restrict a view to certain roles, you would add a second middleware:
const assertRole = ( ...roles ) => ( req, res, next ) =>
req.user && roles.includes( req.user.role ) ? next() : res.sendStatus( 403 ) );
todoRoutes.get("/admin/view1", authenticate, assertRole( "user", "editor", "admin" ), TodoController.getTodos );
If your view is going to be need more information about the user, the authentication API will need to provide that, either in the JWT (and thus guaranteed) or outside (not guaranteed, but results in smaller tokens).
exports.roleAuth = function(roles){
return function(req, res, next){
var user = req.user;
User.findById(user._id, function(err, foundUser){
if(err){
res.render('index.html');
}
if(roles.indexOf(foundUser.role) > -1){
res.render('another.html');
}
res.render('another2.html');
});
}
}
There are two ways that I see this can be done, though not perfectly.
One is to use the primary api endpoint of a particular view. Normally each view has a primary endpoint, and if that endpoint returns an Unauthorized / Forbidden status, you shouldn't render the view. But this solution has problems and not always there's a primary endpoint matching a view.
Other option is to namespace the routes with roles, like admin/dashboard and users/dashboard, and the user should have a field describing their role, eg. user.role. And before rendering the views, check for the respective role in url and in user object returned by the API.
The second option is preferred and this is what I generally use.
Hope it helps.
Doesn't the beauty of JWTs solve this issue already, or rather can solve your issue with the correct implementation?
All that is required is for you to
initialise the passport-jwt module in your WebApp in the same way you initialise it in your API, namely the secretOrKey parameter.
inspect the JWT, specifically the role property and allow/reject based on that
I'm would like to know how could i create a method, or if there is a method to generate a new token only with the email. I want create a option in my site "Send new verification email", where the user only needs to put the email. Actually i'm using Mandril, so i'm using a custom way to send emails and verify users:
function generateVerificationToken(context, user, callback) {
const { req } = context;
req.app.models.User.generateVerificationToken(user, (error, token) => {
if (error) {
return callback(error, null);
}
callback(null, token);
});
}
User.afterRemote('create', (context, user, next) => {
generateVerificationToken(context, user, (error, token) => {
if (error) {
return next(error);
}
user.verificationToken = token;
user.save((error) => {
if (error) {
return next(error);
}
loopback.Email.send({
to: user.email,
template: {
name: 'signup-confirm',
},
global_merge_vars: [{
name: 'href',
content:`http://localhost:3000/api/accounts/confirm?uid=${user.id}&token=${token}&redirect=http://localhost:4200/login/token-verification&verification=1`
}]
}, (error) => {
next(error);
});
});
});
});
Thanks in advance!
(Note: This question is a bit tricky because it involves several modifications that, although not that hard, might require some refactoring of your code. Also, see the warning note at the end please.)
1. Override the User model
(Note: Although you could do this in another model, I found it better to do it inside User for consistency's sake, even though there's a bit more to do.)
To override the User model, you can do two things. Some people like to add a new user model (in lowercase) and do the overriding there, but I personally prefer to use Spencer Mefford's more elegant way of doing it.
You should check the whole gist because there's a lot more going on, but to summarize a bit, you need to create a new boot script, ideally with a name starting with "0" (boot scripts are executed in alphabetical order and thus you need to have the model ready before the rest of the stuff), for example
server/boot/0-user-model-override.js
Then you add the necessary boilerplate:
module.exports = function (app) {
var User = app.models.User;
var Email = app.models.Email;
var Role = app.models.Role;
var RoleMapping = app.models.RoleMapping;
var ACL = app.models.ACL;
/*
* If this is an initial setup, create the ACL entry,
* otherwise just configure the relationships
* (this needs to be done every time the server is started)
*/
if(process.env.INIT_SETUP == "true"){
ACL.create({
model: 'User',
property: 'customEndpoint1',
accessType: 'EXECUTE',
principalType: 'ROLE',
principalId: '$everyone',
permission: 'ALLOW'
}, function (err, acl) { // Create the acl
if (err) console.error(err);
});
}
RoleMapping.belongsTo(User);
RoleMapping.belongsTo(Role);
User.hasMany(Role, {through: RoleMapping, foreignKey: 'principalId'});
User.hasMany(RoleMapping, {foreignKey: 'principalId'});
Role.hasMany(User, {through: RoleMapping, foreignKey: 'roleId'});
// Add your custom endpoints
User.customEndpoint1 = function(param1, cb) {...};
User.remoteMethod('customEndpoint1',...){...};
};
The boilerplate is basically there because we need to manually add an ACL entry that sets the permissions to allow anyone to request a new verification email. If this is not done, by default the User model denies access by non-authenticated users.
Also, note that we only create the ACL entry in the DB when we are doing the initial setup, i.e. we do it only once (unless you setup a new DB).
However we need to configure the relationships between the User, Role and RoleMapping every time we start the server, otherwise you will get access errors for valid users and other strange behaviors.
(Note: Although this is quite a bit of work, I think being able to somewhat easily add new functionality to the User model will allow you to keep user management where it belongs.)
After this setup you can now do e.g.
POST https://myserver/api/Users/newVerificationEmail
2. Create an endpoint to request the new link
To add an endpoint you do as you would with any other model:
User.newVerificationEmail = function(email, cb) {
console.log("A new verification email was requested for: " + email);
cb(null);
};
User.remoteMethod(
'newVerificationEmail',
{
accepts: [
{arg: 'email', type: 'string'}
],
http: {
verb: 'post'
}
}
);
3. Call the verify method and send the email
To send the verification email you have a few options. You can either:
Re-use the User.verify() method (located in the user.js model file) and the default SMTP emailer
Re-use the User.verify() method but with your own Emailer (to send via API for example)
Do everything by hand, i.e. generate the token yourself, saving it to the User collection and then sending the email, which is basically what User.verify() does. However this requires you to also write the confirmation logic which is yet more work.
User.verify() with default emailer
To re-use the verify method you need to generate the verify link (except the token part, which will be added by the method itself), configure the options, and call the method.
User.newVerificationEmail = function(email, cb) {
console.log("A new verification email was requested");
var userModel = User.constructor;
// Note: To get user.id you need to query the DB
// for the User instance with the requested email
var verifyLink = 'https://' +
hostAddress +
':' +
portNumber +
restApiRoot +
'/Users/confirm' +
'?uid=' +
user.id +
'&redirect=https://' + hostAddress + '/verified?user_id='+user.id;
var options = {
type: 'email',
mailer: Email,
to: user.email,
from: 'sender#example.com',
subject: 'My Email Subject',
template: path.resolve(__dirname, '../views/verify.ejs'),
user: user,
verifyHref: verifyLink,
host: myEmailHost,
port: myEmailPort
};
user.verify(options, function(err, response) {
if (err) {
console.log(err);
}
console.log("Account verification email sent to " + options.to);
cb(null);
});
};
Create the email verification template
The email that will be sent is the one specified in options.template, i.e. server/views/verify.ejs
This file should contain the verification link we generated again. You can add whatever HTML you want, just be sure to add the verifyHref variable:
Please click this link to verify your email
After these changes are done, this should send an email whenever you do a POST request to api/Users/newVerificationLink
User.verify() with custom emailer
I haven't yet finished implementing this solution, but it basically involves creating your own Email connector to use your provider's API (e.g. Mandrill, Mailgun, etc) and passing this model in the options.mailer field.
Warning: I haven't tested this code and there are several variable values that you need to specify yourself (e.g. hostAddress, portNumber, restApiRoot, etc). The code in this answer has been extracted from several pieces of a project I'm working on and although it's almost complete, you need to verify that there are no missing callbacks and other typos and compiler errors as well as provide the code to search the User object corresponding to the provided email (which is very easy to do).
I have the following code
var user = function(req,res,next) {
db.findOne({ username: req.params.uid }, function (err, docs) {
//error handaling
if(err){console.log(err)}
//check if user is real
if(docs === null){
res.end('404 user not found');
}else{
//IMPORTANT PART res.sendFile(__dirname + '/frontend/user.html');
}
});
}
app.get('/user/:uid',user);
Don't worry about the database stuff.
I want to know how to get req.params.uid sent to the client side and how to get it from there.
Thanks so much.
If your user is configured correctly every request will have a user:
var user = function(req,res) {
db.User.findOne({ _id: req.user._id }, function (err, docs) {
//error handaling
if(err){console.log(err)}
//check if user is real
if(docs === null){
res.end('404 user not found');
}else{
res.json(docs)
}
});
and then your api endpoint is just '/user/
In your client just make a GET request to this endpoint (maybe using AJAX) and your response will be any user that makes that given request.
Note: You don't need to pass in next unless you are defining middleware.
This is just a more complete answer based on my comment.
If you want to store a string of information about the user with each request they make, then you want to use cookies.
When the user first makes a request to the page, you would set the cookie via the res.cookie. So, in your code, the final if statement would look something like:
if(docs === null) {
res.end('404 user not found');
} else {
res.cookie('uid', req.params.uid, { httpOnly: true });
//IMPORTANT PART res.sendFile(__dirname + '/frontend/user.html');
}
Then, in the next request, and futures requests before the cookie expires, you can access it using:
req.cookies.uid
However, you need the cookie-parser middleware somewhere in your app beforehand:
var cookieParser = require('cookie-parser');
app.use(cookieParser());
If you need to access the value of uid on the clientside, you could either use a template, or set the httpOnly value to false when setting it using res.cookie. Then you could access the cookie using document.cookies.
Check out this W3Schools page for accessing cookies on the clientside.
I'm currently working on a angular + sails project. I'm using json web tokens for auth. It works fine but I wanna set a new token for every validated request that my angular app does.
This is my auth policy
passport.authenticate('jwt', function (error, user, info) {
if (error) return res.serverError(error);
if (!user)
return res.send({
message: info.message,
code: info.code,
tokenError: info.name
});
// The token is ok past this line
// I check the user again
User.findOne({ email: user.email }, function (err, thisUser) {
if (err) { return res.send(err); }
if (!thisUser) {
// send a bad response
}
req.user = user;
// This is the new token that I wanna send to the frontend
var newToken = AuthService.createToken(thisUser);
next();
});
})(req, res);
With this policy I can create the new token, but then I would need a way to include this token in every response, this Is the point where I'm stuck.
I gues I could do it manually in every controller action, but this is want I want to avoid
The best way to standardize your responses in Sails is to use the custom responses feature. In short, instead of calling res.send() or res.json() in your controller actions, call res.ok() instead, and then customize the api/responses/ok.js file that is generated with every new Sails app. This is the same response that Sails blueprints use as well!
In your case, you'd want to save the token onto the request object (e.g. req.token) in your policy code, then use that property in your logic inside of ok.js.