I'm starting to experiment with ExpressJS + AngularJS and I've run into a stressful situation.
My goal is to have one login page and one dashboard page, and using Passport + MongoDB I'll authenticate a user in and, if credentials are correct, redirect him to the dashboard page.
I've started with the angular-express-seed and modified it so I ended up with the previously mentioned views:
$routeProvider.
when('/login', {
templateUrl: 'partials/login',
controller: 'LoginCtrl'
}).
when('/dashboard', {
templateUrl: 'partials/dashboard',
controller: 'DashboardCtrl'
}).
otherwise({
redirectTo: '/login'
});
I'm serving each partial view with:
app.get('/partials/:name', routes.partials);
exports.partials = function (req, res) {
var name = req.params.name;
res.render('partials/' + name);
};
The login system is working correctly except for the part that I can access the dashboard directly (without loging in at all).
So I guess I need to figure out if the user if authenticated, before granting him access to the dashboard (or any other area for that matter).
I found this that does the trick:
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) { return next(); }
res.redirect('/login');
}
and I would call it on every GET to a partial
app.get('/partials/:name', ensureAuthenticated, routes.partials);
The big problem here is that the login view is also a partial, and I don't want ensureAuthenticated to run when that's the view being requested.
Currently calling ensureAuthenticated on every partial request crashes any browser, no errors whatsoever.
What I've tried so far (and did not work):
function ensureAuthenticated(req, res, next) {
if ( req.route.params.name !== 'login' && req.isAuthenticated() ) { return next(); }
res.redirect('/login');
}
Also tried calling the login view with its own app.GET and not using ensureAuthenticated but somehow Angular flips off and does not even load.
Any ideas on how to resolve this ?
Thanks in advance.
It's unclear to me if the first (unmodified) version of ensureAuthenticated crashes your browsers, or the last version (in which you check for /partials/login), although no matter what, it shouldn't happen (if by 'crashing' you mean 'stalls', that's a tell-tale sign of Express not sending back a response)
But as an alternative, try this:
app.get('/partials/login', routes.partials);
app.get('/partials/:name', ensureAuthenticated, routes.partials);
Your routes.partials does have to check explicitly if it's called for the login route, because in that case the name parameter won't exist in req.params.
Just to make sure: do you have a server-side handler for /login as well? Otherwise the res.redirect('/login') isn't going to work.
I don't think that server side redirecting on XHR request is a good practice. XHR request should return 401 if the user is not authorized and the client side (Angular) should take the action as displaying notification message and redirecting user to the login partial. Look at this project for handling such requests. There are demo and blog post.
Related
Rewriting this question to be clearer.
I've used passport-facebook to handle login with facebook on my site.
My front end is in Angular so I know now need to understand whats the correct way of calling that api route. I already have several calls using Angular's $http service - however as this login with facebook actually re-routes the facebook page can i still use the usual:
self.loginFacebook = function )() {
var deferred = $q.defer();
var theReq = {
method: 'GET',
url: API + '/login/facebook'
};
$http(theReq)
.then(function(data){
deferred.resolve(data);
})
return deferred.promise;
}
or is it perfectly ok/secure/correct procedure to directly hit that URL in a window location:
self.loginFacebook = function (){
$window.location.href = API + '/login/facebook';
}
Furthermore, from this how do I then send a token back from the API? I can't seem to modify the callback function to do that?
router.get('/login/facebook/callback',
passport.authenticate('facebook', {
successRedirect : 'http://localhost:3000/#/',
failureRedirect : 'http://localhost:3000/#/login'
})
);
Thanks.
I was stacked on the same problem.
First part:
I allow in backend using cors and in frontend i use $httpProvider, like this:
angular.module('core', [
'ui.router',
'user'
]).config(config);
function config($httpProvider) {
$httpProvider.defaults.useXDomain = true;
$httpProvider.defaults.headers.common['X-Requested-With'];
$httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
};
The second part:
<span class="fa fa-facebook"></span> Login with facebook
This call my auth/facebook route that use passport to redirect to facebook page allowing a user to be authenticated.
If the user grant access, the callback /api/auth/facebook/callback is called and the facebook.strategy save the user with the profile data.
After saving the user, i create a special token with facebook token, id and email. This info is used to validate every time the user access to private states in the front.
My routes are something like this:
router.get('/facebook', passport.authenticate('facebook',
{ session: false, scope : 'email' }));
// handle the callback after facebook has authenticated the user
router.get('/facebook/callback',
passport.authenticate('facebook',
{session: false, failureRedirect: '/error' }),
function(req, res, next) {
var token = jwt.encode(req.user.facebook, config.secret);
res.redirect("/fb/"+token);
});
In frontend i catch the /fb/:token using a state and assign the token to my local storage, then every time the user go to a private section, the token is sent to backend and validate, if the validation pass, then the validate function return the token with the decoded data.
The only bad thing is that i don't know how to redirect to the previous state that was when the user click on login with facebook.
Also, i don't know how you are using the callback, but you need to have domain name to allow the redirect from facebook. I have created a server droplet in digitalocean to test this facebook strategy.
In the strategy you have to put the real domain in the callback function, like this:
callbackURL: "http://yourdomain.com/api/auth/facebook/callback"
In the same object where you put the secretId and clientSecret. Then, in your application in facebook developers you have to allow this domain.
Sorry for my english, i hope this info help you.
Depending on your front-end, you will need some logic that actually makes that call to your node/express API. Your HTML element could look like
<a class='btn' href='login/facebook'>Login</a>
Clicking on this element will make a call to your Express router using the endpoint of /login/facebook. Simple at that.
Is there a way to force the user to log in first in an app generated by the angular full stack yeoman ?
I tried to add the following code in the run part of app.js but was not successful
Auth.isLoggedIn(function(loggedIn) {
console.log(loggedIn);
if (!loggedIn) {
console.log("redirecting");
// event.preventDefault();
$state.go('login');
}
});
I found authentication controls in api index.js files but none for the / landing page ...
Thx
I did not use google as i should have !
To force authentication for a state, just add
authenticate: true
in the state (or all states in my case)
Without more code or information on which router you are using (generator-angular-fullstack supports both the default NgRouter and UIRouter) it is tough to give a complete answer. By your answer to your question I am assuming you have UI Router and have figured out how to do client side authentication within the generated fullstack code. However, you will also need to implement something similar to what they have done in their 'users' api to protect your api end points on the server side and return a 401/403 error.
'use strict';
var express = require('express');
var controller = require('./user.controller');
var config = require('../../config/environment');
var auth = require('../../auth/auth.service');
var router = express.Router();
router.get('/', auth.hasRole('admin'), controller.index);
router.delete('/:id', auth.hasRole('admin'), controller.destroy);
router.get('/me', auth.isAuthenticated(), controller.me);
router.put('/:id/password', auth.isAuthenticated(), controller.changePassword);
router.get('/:id', auth.isAuthenticated(), controller.show);
router.post('/', controller.create);
module.exports = router;
In the above code (which can be found by navigating to the server folder, then the api folder, then the user folder and looking at index.js) you will see that they are calling a couple of functions.
They are calling auth.hasRole('admin') and auth.isAuthenticated().
Those are functions which can be found in the server side auth/role service under the folder auth and in the auth.service.js file.
function hasRole(roleRequired) {
if (!roleRequired) throw new Error('Required role needs to be set');
return compose()
.use(isAuthenticated())
.use(function meetsRequirements(req, res, next) {
if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) {
next();
}
else {
res.status(403).send('Forbidden');
}
});
}
I think it is important to understand how this is working on the server side also. So, if you navigate to localhost:9000/admin and open console you will see that there is a 401 or 403 error depending on whether or not you are logged in and/or logged in as an admin user.
Just Paste authenticate:true on main.js
angular.module('testcesarApp')
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'app/main/main.html',
controller: 'MainCtrl',
authenticate:true
});
});
In a single page application using angular routing, how can I redirect a page after an api call. In my case, I want to redirect the user to the profile page after they have called the login api. So this is what I thought would work but it isn't.
On the client, main.js. I have the angular routing set up
app.config(function($routeProvider){ $routeProvider
//the home page display
.when('/', {
templateUrl: 'main.html',
controller: 'mainController'
})
.when('/login', {
templateUrl: 'login.html',
controller: 'loginController'
})
.when('/signUp', {
templateUrl: 'signUp.html',
controller: 'signUpController'
})
.when('/profile', {
templateUrl: 'profile.html',
//controller: 'mainController'
}); });
and from my controller I call the /login post api
app.controller('authController', function($scope, $http, $rootScope, $location){
$scope.user = {username: '', password: ''};
$scope.error_message = '';
$scope.login = function(){
$http.post('/login', $scope.user).success(function(data){
if(data.state == 'success'){
//set username authenticated property to true after successful log in
//I am only pasting some of my code here, more logic before controller
$rootScope.authenticated = true;
$rootScope.current_user = "james";
$location.path('/profile');
}
else{
$scope.error_message = data.message;
}
});
};
});
and here is my login api
router.post('/login', passport.authenticate('local-login', {
successRedirect : '/success', // redirect to the secure profile section
failureRedirect : '/failure', // redirect back to the signup page if there is an error
failureFlash : true // allow flash messages
}));
and when it succeeds, it calls success which sends back the data which should trigger the callback in $http.post and redirect the page through $location.path('/profile');. However, the callback isn't called and my page displays the user information from res.send
//sends successful login state back to angular
router.get('/success', function(req, res){
res.send({state: 'success', user: req.user ? req.user : null});
});
Am I on the right track? I am just following microsoft's tutorial https://www.microsoftvirtualacademy.com/en-us/training-courses/mean-stack-jump-start-8442 but their completed page on github doesn't even work so it doesn't help me debug this problem of mine.
Using successRedirect and failureRedirect in passport will redirect the client to the specified pages, which will prevent your client-side angularJS routing from taking place. The reason you're seeing the user info after logging in is because your client is being redirected to the /success page, rather than actually responding to the original request. The client then fetches the success page with a GET request, and the new GET request is then responded to with the user info.
I would suggest leaving the node.js redirects out when using AngularJS, since you probably want to handle redirection on the client side:
router.post('/login', passport.authenticate('local-login'), function(req, res){
res.send(req.user);
});
The inline function will never execute if the user is not authenticated. Instead, passport will respond directly with a 401 error status, with a body of "Unauthorized". Therefore the success state is not required. On the client side, you should use the .error() function to deal with 401 errors, rather than checking your state variable:
$http.post('/login', $scope.user).success(function(user){
$rootScope.authenticated = true;
$rootScope.current_user = "james";
$location.path('/profile');
})
.error(function(err){
$scope.error_message = err;
});
If you want to pass back a more specific reason as to why the request was unauthorized (which is not always a good idea), you can use flash messages, and issue another GET request from angular to get a more detailed authorization failure message.
You seem to have a slight impedance mismatch on what the front-end and back-end want to do here. Your AngularJS code expects to make a POST to the API endpoint and get back a 200 (success) along with some JSON data which tells it about the success or failure of the login attempt.
The back-end, thinks it's going to receive a POST and then redirect the caller to a new location. At least that's the way I'm reading it. It's not simply sending back some data with an HTTP response code of 200 or an error code.
I think you want to tweak the back-end code to simply return data to the front-end to get the result you expect.
So far I haven't seen success in making Ajax calls to API redirecting to a page. We have a similar situation where API call may result in redirecting to a error page. We wanted to handle that in the server rather than asking UI (Angular) to do it. But it's just frustrating to see none of the methods of redirect like res.redirect are working.
Our scenario is Angular makes a API call through Ajax and API running on Node.js should redirect to a html page.
I am using Node.JS with Express, Angular.JS and the node module connect-roles for ACL. I want to allow a user with user.status of "Platinum" to access "Platinum" but not "Gold" and vice versa.
I have the ACL part working, if I enter /Platinum into the navigation bar I can't access /Gold, but when I try to access /Platinum I only get the template but not the root shell, so what comes up is this:
You made it!
You have the {{status}} status!
If I click on a link in angular to /Platinum, everything works as it should. If I enter any neutral address in the navigation bar, everything works as it should.
This should be an easy fix, but I've not figured it out.
Here is the code that sets up authorizations, I'm pretty sure everything here is okay.
ConnectRoles = require('connect-roles')
var user = new ConnectRoles({
failureHandler: function(req, res, action){
var accept = req.headers.accept || '';
res.status(403);
if(accept.indexOf('html')) {
res.render('access-denied', {action: action});
} else {
res.send('Access Denied - You don\'t have permission to: ' + action);
}
}
});
var app = express();
app.use(user.middleware());
// Setting up user authorizations,
// i.e. if req.user.status = "Platinum", they are given Platinum status
user.use('Platinum', function(req) {
if (req.user.status == 'Platinum') {
return true;
}
});
user.use('Gold', function(req) {
if (req.user.status == 'Gold') {
return true;
}
});
user.use('Admin', function(req) {
if (req.user.status == 'Admin') {
return true;
}
});
That sets up authorizations, now the problem lies below with the routing.
app.post('/login', passport.authenticate('local',
{ successRedirect: '/', failureRedirect: '/login' }));
app.get('/Platinum', user.is('Platinum'), function(req, res) {
//Obviously the code below is wrong.
res.render('templates/support/Platinum');
});
app.get('/Gold', user.is('Gold'), function(req, res) {
res.render('templates/support/Gold');
});
The way you are configuring your routes on server side (using express) is not correct. For a single page app like AngularJS, you need to do all of the routing for pages on the client (i.e. in Angular). The server still defines routes for API requests (e.g. getting and posting data) and static resources (index.html, partial HTML files, images, javascript, fonts, etc), though.
Thus the following code is wrong in your server side JS:
app.get('/Platinum', user.is('Platinum'), function(req, res) {
//Obviously the code below is wrong.
res.render('templates/support/Platinum');
});
app.get('/Gold', user.is('Gold'), function(req, res) {
res.render('templates/support/Gold');
});
Just remove those lines.
Instead, you need to define the routes that the server will handle, such as your /login post one first, and how to get static files (I suggest prefixing them all with /pub in the URL). Then you need to do something like the technique in this answer to return your index.html page if no routes are matched.
That way, when a user types http://localhost:port/Gold, express will see there is no route defined for /Gold, so it will return index.html, which will load AngularJS, run your Angular app, which will then look at the URL and see if that matches any of the routes your AngularJS app has configured, and if so, fetch the partial for that page and insert it into your ng-view (if using the core router).
I have in my a .cshtml file a view that has a button that "updates"
The update button works but I want to redirect to home directory.
In my .js file i first had
success(function (data) {
//Showing Success message
alert("Incident Added");
$location.path("\Home");
})
The link went from /Home/Create to /Home/Create#/Home and just stays there. I would like it to go back to my home directory. Essentially going back to the home view after Create.
I also tried this
success(function (data) {
//Showing Success message
alert("Incident Added");
$scope.$apply(function () {
$location.path("/Home");
});
})
And the url didn't add the #/Home afterwards but it stayed the same of /Home/Create and didnt' go to /Home. I would like to know what do I need to do to get it go back home. Or rather learn how to do redirects properly.
Assuming your $routProvider has this set for home
$routeProvider
.when('/', { templateUrl:'home.html',controller:'CtrlHome'})
you should be able to just use
$location.path("/");
$location service designed for single page application it works with $route service and easily be misunderstood
if you want a basic HTTP Redirect to MVC route (eg. your.app.com/Home) use $window.location
$window.location.href = "/Home";