I have the following Controller for my login page:
// Authentication Controller
// the basics of Passport.js to work.
var AuthController = {
// localhost:1337/login Render the login page
// <form role="form" action="/auth/local" method="post">
// <input type="text" name="identifier" placeholder="Username or Email">
// <input type="password" name="password" placeholder="Password">
// <button type="submit">Sign in</button>
// </form>
login: function(req, res) {
var strategies = sails.config.passport,
providers = {};
// Get a list of available providers for use in templates.
Object.keys(strategies).forEach(function(key) {
if (key === 'local') return;
providers[key] = {
name: strategies[key].name,
slug: key
};
});
// Render the `auth/login.ext` view
res.view({
providers: providers,
errors: req.flash('error')
});
},
// Log out a user and return them to the homepage
// Passport exposes a logout() function on req (also aliased as logOut()) that
// can be called from any route handler which needs to terminate a login
// session. Invoking logout() will remove the req.user property and clear the
// login session (if any).
logout: function(req, res) {
req.logout();
res.redirect('/login');
},
// The registration form is Just like the login form
register: function(req, res) {
res.view({
errors: req.flash('error')
});
},
// Create a third-party authentication endpoint
provider: function(req, res) {
passport.endpoint(req, res);
},
// Create a authentication callback endpoint
// This endpoint handles everything related to creating and verifying Pass-
// ports and users, both locally and from third-aprty providers.
// Passport exposes a login() function on req (also aliased as logIn()) that
// can be used to establish a login session. When the login operation
// completes, user will be assigned to req.user.
callback: function(req, res) {
passport.callback(req, res, function(err, user) {
req.login(user, function(err) {
// If an error was thrown, redirect the user to the login which should
// take care of rendering the error messages.
if (err) {
res.redirect('/login');
}
// Upon successful login, send the user to the homepage were req.user
// will available.
else {
res.redirect('/');
}
});
});
}
};
module.exports = AuthController;
I am using Mocha as my test framework. The application is based on Sails.
How would I write Mocha test cases and run them on the provided Controller?
I'm using supertest to call controllers as a user, to do so, first lift the sails in the before function so it can be used as the server by supertest.
before(function (done) {
Sails.lift({
// configuration for testing purposes
log: {
//level: 'silly'
level: 'silent'
},
hooks: {
grunt: false,
},
}, function (err, sails) {
done(err, sails);
});
}
Then initialize supertest with the url of your sails app
request = require('supertest');
agent = request.agent(appurl);
You can now write test to post / get data to test your controller from frontend as would a client.
it('should do the post and return whatever', function (done) {
agent.post('/controller/function')
.send(the_post_object_with_params)
.end(function (err, res) {
if (err) {
console.log(err);
}
console.log(res.body); // the content
done();
});
}
I think the main thing is to include sails application into your testcase. I also found no examples with controllers tests but some with models:
Is it possible to use the Mocha Framework against a model in Sails.js?
Cannot unit test my model in sailsjs
Related
I am implementing an auth0 login flow for a node js server that serves a react app. I have implemented the login and log out flows correctly, but in the /callback URL I am given a false token that cannot be decrypted.
So, if I visit /login, it takes me to the Auth0 form correctly; and I can log in successfully, according to the logs in Auth0. But when Auth0 redirects back to the callback url, I get a false token.
app.get("/callback", (request, response, next) => {
passport.authenticate("auth0", (auth0Error, token) => {
if (!token){
// HERE, token is false
}
...
})(request, response, next);
});
What could cause this false token? How does the authenticate function work in a callback? And how should I handle this? Should I try auth0 again?
To understand the issue, I started the project in debug mode and added some breakpoint. If you look carefully, passport.authenticate() method calls Auth0Strategy . Then, Auth0Strategy exchange code for token (Authorization Code flow) and returns user profile. Therefore, in the the callback route , token is not available. I added the following lines of code to access accessToken in the callback route.
const strategy = new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL: '/callback',
},
function(accessToken, refreshToken, extraParams, profile, done) {
// accessToken is the token to call Auth0 API (not needed in the most cases)
// extraParams.id_token has the JSON Web Token
// profile has all the information from the user
console.log(JSON.stringify(profile))
console.log(accessToken);
return done(null, profile, accessToken); // Passing access token
}
);
Then , callback route should have accessToken available.
router.get('/callback', function (req, res, next) {
passport.authenticate('auth0', function (err, user, token) {
if (err) { return next(err); }
if (!user) { return res.redirect('/login'); }
req.logIn(user, function (err) {
if (err) { return next(err); }
const returnTo = req.session.returnTo;
delete req.session.returnTo;
res.redirect(returnTo || '/user');
});
})(req, res, next);
});
This was added just to explain the flow. But you can ignore passing the token for production application.
How can i directly authenticate the user after signup.
Below are the the deatail of serializeUser and deserializeUser.
passport.serializeUser(function(user, done) {
done(null, {tutorId: user.tutorId, userType: user.userType});
});
passport.deserializeUser(function(userData, done) {
Tutor.getTutorById(userData.tutorId, (err, user) => {
if (err) {
try {
logger.silly(`message: POST inside passport.deserializeUser; file: index.js; error: ${err}; user: ${JSON.stringify(user)}`);
logger.error(`message: POST inside passport.deserializeUser; file: index.js; error: ${err}; user: ${JSON.stringify(user)}`);
} catch (e) {
You can use req.login() after successful registration.
From official Passport documentation:
Note: passport.authenticate() middleware invokes req.login()
automatically. This function is primarily used when users sign up,
during which req.login() can be invoked to automatically log in the
newly registered user.
A sample registration code might look like this:
router.post("/register",(req,res) => {
var user = new User();
user.name = req.body.name;
user.email = req.body.email;
//define other things here
//create hash and salt here
user.save().then(user => {
//on successfult registration
//login user here, using req.login
req.login(user ,err => {
if(!err){
//redirect to logged-in page
//or user page
res.redirect('/')
}
})
})
})
Read about req.login() in official passport documentsation
I hope this helps you out.
you can create token just after successful registration and send it back in registration response
I am currently having a problem authenticating user in my SailsJS app using the PassportJS.
I have generated all the stuff needed for authentication using sails-generate-auth according to this tutorial.
It seems like the POST request is routed correctly as defined in the routes.js file:
'post /auth/local': 'AuthController.callback',
'post /auth/local/:action': 'AuthController.callback',
In AuthController I've got following code:
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
// Upon successful login, send the user to the homepage were req.user
// will be available.
res.redirect('/user/show/' + user.id);
});
});
},
I am submitting my login form in Jade template:
form(role='form', action='/auth/local', method='post')
h2.form-signin-heading Please sign in
.form-group
label(for='username') Username
input.form-control(name='username', id='username', type='username', placeholder='Username', required='', autofocus='')
.form-group
label(for='password') Password
input.form-control(name='password', id='password', type='password', placeholder='Password', required='')
.form-group
button.btn.btn-lg.btn-primary.btn-block(type='submit') Sign in
input(type='hidden', name='_csrf', value='#{_csrf}')
I have checked the values passed to the callback function specified for password.callback(..) call:
passport.callback(req, res, function (err, user, challenges, statuses) {
the user variable is set to "false". I suppose, this is where the error comes from.
What is interesting is, that when the callback() function is called after user registration, the user variable is set right to user object containing all the values like username, email, etc.
If you would like to check other source files, my project is available on github in this repository.
Any help is appreciated.
Shimon
You're using identifier as the usernameField for LocalStrategy (i.e. the default setting) and have username in the login view, which means the authentication framework receives no username and fires a Missing Credentials error.
Either change the field name in the login view to identifier or set the appropriate usernameField through the passport config file (config/passport.js):
local: {
strategy: require('passport-local').Strategy,
options: {
usernameField: 'username'
}
}
In my mean js app i have an account model and corresponding routes and controllers. To remove a specific account i need to have authorization and I need to be logged in.
All users can however list all accounts, I only wont to list the accounts created by the specific user. So i need to add autorization to the list part of the code.
I update the routes for app.route('/accounts') with users.requiresLoginandaccounts.hasAuthorization as shown below:
module.exports = function(app) {
var users = require('../../app/controllers/users.server.controller');
var accounts = require('../../app/controllers/accounts.server.controller');
// Accounts Routes
app.route('/accounts')
.get(users.requiresLogin,accounts.hasAuthorization,accounts.list)
.post(users.requiresLogin, accounts.create);
app.route('/accounts/:accountId')
.get(users.requiresLogin, accounts.hasAuthorization,accounts.read)
.put(users.requiresLogin, accounts.hasAuthorization, accounts.update)
.delete(users.requiresLogin, accounts.hasAuthorization, accounts.delete);
// Finish by binding the Account middleware
app.param('accountId', accounts.accountByID);
};
Now I get an errror since req is not provided with user.
GET /modules/accounts/views/list-accounts.client.view.html 304 8.266
ms - - TypeError: Cannot read property 'user' of undefined
at exports.hasAuthorization (/Users/david/Repositories/budget/app/controllers/accounts.server.controller.js:103:17)
So I imagine i need to update the accounts.server.controller somehow. The delete account does provide an account in the req, so that only the creator can delete as I mentioned earlier. How do I update the code so that the "List of accounts" part work and list only the accounts belonging to that specific user?
/**
* Delete an Account
*/
exports.delete = function(req, res) {
var account = req.account ;
account.remove(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(account);
}
});
};
/**
* List of Accounts
*/
exports.list = function(req, res) {
Account.find().sort('-created').populate('user', 'displayName').exec(function(err, accounts) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(accounts);
}
});
};
/**
* Account middleware
*/
exports.accountByID = function(req, res, next, id) {
Account.findById(id).populate('user', 'displayName').exec(function(err, account) {
if (err) return next(err);
if (! account) return next(new Error('Failed to load Account ' + id));
req.account = account ;
next();
});
};
/**
* Account authorization middleware
*/
exports.hasAuthorization = function(req, res, next) {
if (req.account.user.id !== req.user.id) {
return res.status(403).send('User is not authorized');
}
next();
};
The account client service only contains the basic stuff:
//Accounts service used to communicate Accounts REST endpoints
angular.module('accounts').factory('Accounts', ['$resource',
function($resource) {
return $resource('accounts/:accountId', { accountId: '#_id'
}, {
update: {
method: 'PUT'
}
});
}
]);
And the user object is not mentioned in the controller.
The accounts.hasAuthorization assume it get executed after the accounts.accountById ,on your current configuration req.account will be undefined.
I'm assumming that somewhere in your account model you have:
user: {
type: Schema.ObjectId,
ref: 'User'
}
If you want the user only have access only to the accounts he/she owns :
Change accounts.list route only to requires Login and this gives us access to the req.user :
app.route('/accounts')
.get(users.requiresLogin,accounts.list)
Change the exports.list in the accounts controller:
exports.list = function(req, res) {
Account.find({user: req.user._id}).sort('-created')
.... //
};
I have some Sails.js API tests (using Mocha) that make use of SuperTest's .end() method to run some Chai assertions on the response.
I call the test's done() callback after the assertions, but if an assertion error is thrown, the test times out.
I can wrap the assertions in a try/finally, but this seems a bit icky:
var expect = require('chai').expect;
var request = require('supertest');
// ...
describe('should list all tasks that the user is authorized to view', function () {
it('the admin user should be able to see all tasks', function (done) {
var agent = request.agent(sails.hooks.http.app);
agent
.post('/login')
.send(userFixtures.admin())
.end(function (err, res) {
agent
.get('/api/tasks')
.expect(200)
.end(function (err, res) {
try {
var tasks = res.body;
expect(err).to.not.exist;
expect(tasks).to.be.an('array');
expect(tasks).to.have.length.of(2);
} finally {
done(err);
}
});
});
});
});
Any suggestions on how to better deal with this? Perhaps Chai HTTP might be better?
According to Supertest's documentation, you need to check for the presence of err and, if it exists, pass it to the done function. Like so
.end(function (err, res) {
if (err) return done(err);
// Any other response-related assertions here
...
// Finish the test
done();
});
You can pass out login logic from test.
// auth.js
var request = require('supertest'),
agent = request.agent;
exports.login = function(done) {
var loggedInAgent = agent(sails.hooks.http.app);
loggedInAgent
.post('/login')
.send(userFixtures.admin())
.end(function (err, res) {
loggedInAgent.saveCookies(res);
done(loggedInAgent);
});
};
And then just use it in your test:
describe('should list all tasks that the user is authorized to view', function () {
var admin;
before(function(done) {
// do not forget to require this auth module
auth.login(function(loggedInAgent) {
admin = loggedInAgent;
done();
});
});
it('the admin user should be able to see all tasks', function (done) {
admin
.get('/api/tasks')
.expect(function(res)
var tasks = res.body;
expect(tasks).to.be.an('array');
expect(tasks).to.have.length.of(2);
})
.end(done);
});
it('should have HTTP status 200', function() {
admin
.get('/api/tasks')
.expect(200, done);
});
});
With such approach you should not login your admin for each test (you can reuse your admin again and again in your describe block), and your test cases become more readable.
You should not get timeouts with this approach because .end(done) guaranties that your test will finish without errors as well with them.