Proper way to handle authentication - angularjs

I have read a lot of different examples on how to implement authentication in an Angular app. I am using Firebase Simple Login as a (No-)Backend.
Users should only see any content of my app, once they are logged in. If they are logged out, all they can see is /auth/login and /auth/register. Currently, I have the following solution which works. But:
1) I am not sure whether this is bullet-proof because...
2) ... when I open /home manually while NOT logged in, I correctly get redirected to /auth/login but the HomeCtrl gets executed anyway. No sensitive data is exposed because no data is returned from the Backend, but there must be a way to NOT execute the controller at all if I am logged out?
3) Everytime I expose sensitive data, do I have to check inside my controller if the user is authenticated over and over again?
Bonus question: How do I redirect to /home after successful login? Currently, inside my AuthService I do $location.path('home'); on successful login, but this doesn't account for the state?!
My app.js:
angular.module('myApp', ['ionic', 'firebase', 'myApp.services', 'myApp.controllers'])
.run(function($rootScope, $location, AuthService) {
$rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) {
/**
* AuthService.isLoggedIn() returns TRUE or FALSE
* depending on whether user authenticated successfully
* against the Firebase backend
*/
// redirect to login
if (!AuthService.isLoggedIn() && to.name !== 'auth.register') {
$location.path('/auth/login');
}
});
})
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('auth', {
url: "/auth",
abstract: true,
templateUrl: "templates/auth-tabs.html",
controller: 'AuthCtrl'
})
.state('auth.login', {
url: '/login',
views: {
'login-tab': {
templateUrl: 'templates/auth-login.html'
}
}
})
.state('auth.register', {
url: '/register',
views: {
'register-tab': {
templateUrl: 'templates/auth-register.html'
}
}
})
.state('home', {
url: '/home',
templateUrl: 'templates/home.html',
controller: 'HomeCtrl'
});
$urlRouterProvider.otherwise('/home');
});

The way I've implemented it is by handling a 401 http server response, because I don't want to worry about checking for the user authentication state, I prefer letting the server handle that. That being said.
Documentation on $urlRouter.sync() specifies that if you prevent the default event, you may manually trigger the succesChange
angular.module('app', ['ui.router']);
.run(function($rootScope, $urlRouter) {
$rootScope.$on('$locationChangeSuccess', function(evt) {
// Halt state change from even starting
evt.preventDefault();
// Perform custom logic
var meetsRequirement = ...
// Continue with the update and state transition if logic allows
if (meetsRequirement) $urlRouter.sync();
});
});
$urlRouter documentation

Related

How to secure Angular routes with Firebase v3?

I want to protect various routes on my site using Firebase V3, AngularJS and ui.router.
This looks like a similar issue. I've followed the steps from that SO post but its not working for me.
What I expect to happen:
When clicking the FAQ link I should be forwarded to the login page if logged out and should display the FAQ page when logged in.
What actually happens:
FAQ page isn't accessible at all. Logging in doesn't make any difference. It also doesn't forward me to the login page when logged out.
I'm getting this error within my run function.
ReferenceError: Firebase is not defined(…)
I've included AngularFire on the page, if I don't I get a module injector error even if I remove Firebase from the dependency array.
var app = angular.module('app', ['ui.router', 'firebase']);
app.constant('FirebaseDatabaseUrl', 'https://myfbdb.firebaseio.com');
app.config(function($stateProvider, $urlRouterProvider, $firebaseRefProvider, FirebaseDatabaseUrl) {
$firebaseRefProvider.registerUrl(FirebaseDatabaseUrl);
// If a route other than status is requested,
// go to the auth route
//$urlRouterProvider.otherwise('/logintest/login');
$stateProvider
.state('login', {
url: '/login',
templateUrl: 'pages/login.html',
controller: 'LoginController as login'
})
.state('faq', {
url: '/faq',
templateUrl: 'pages/faq.html',
controller: 'FaqController as faq',
resolve: {
// controller will not be loaded until $requireSignIn resolves
"firebaseUser": ["$firebaseAuthService", function($firebaseAuthService) {
console.log('waitForSignIn')
// $waitForSignIn returns a promise so the resolve waits for it to complete
return $firebaseAuthService.$waitForSignIn();
}]
}
})
.state('about', {
url: '/about',
templateUrl: 'pages/about.html',
controller: 'AboutController as about',
resolve: {
// controller will not be loaded until $requireSignIn resolves
"firebaseUser": ["$firebaseAuthService", function($firebaseAuthService) {
// If the promise is rejected, it will throw a $stateChangeError
return $firebaseAuthService.$requireSignIn();
}]
}
})
});
app.controller('FaqController', ['$scope', 'firebaseUser', function($scope, firebaseUser){
console.log('faq')
}]);
app.run(["$rootScope", "$state", function($rootScope, $state) {
console.log('run');
$rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) {
// We can catch the error thrown when the $requireSignIn promise is rejected
// and redirect the user back to the home page
if (error === "AUTH_REQUIRED") {
console.log('redirecting to login page')
$state.go("login");
}
});
}]);
AngularFire versions 2.0+ are compatible with Firebase 3.0. Anything below AngularFire 2.0 is for the legacy version of Firebase.

AngularJs not redirecting

For some reason after adding:
.state('root', {
abstract: true,
url: '',
views: {
'layout': {
templateUrl: '../views/layout.html'
},
'header#root': {
templateUrl: '../views/shared/header.html'
},
'footer#root': {
templateUrl: '../views/shared/footer.html'
}
}
})
it stopped redirecting if the user is not logged in.
here is the full code:
angular
.module('authApp', ['ui.router', 'ui.bootstrap', 'ngAnimate', 'satellizer', 'smart-table'])
.config(function ($stateProvider, $urlRouterProvider, $authProvider, $httpProvider, $provide) {
function redirectWhenLoggedOut($q, $injector) {
return {
responseError: function (rejection) {
// Need to use $injector.get to bring in $state or else we get
// a circular dependency error
var $state = $injector.get('$state');
// Instead of checking for a status code of 400 which might be used
// for other reasons in Laravel, we check for the specific rejection
// reasons to tell us if we need to redirect to the login state
var rejectionReasons = ['token_not_provided', 'token_expired', 'token_absent', 'token_invalid'];
// Loop through each rejection reason and redirect to the login
// state if one is encountered
angular.forEach(rejectionReasons, function (value, key) {
if (rejection.data.error === value) {
// If we get a rejection corresponding to one of the reasons
// in our array, we know we need to authenticate the user so
// we can remove the current user from local storage
localStorage.removeItem('user');
// Send the user to the auth state so they can login
$state.go('root.login');
}
});
return $q.reject(rejection);
}
}
}
// Setup for the $httpInterceptor
$provide.factory('redirectWhenLoggedOut', redirectWhenLoggedOut);
// Push the new factory onto the $http interceptor array
$httpProvider.interceptors.push('redirectWhenLoggedOut');
$authProvider.loginUrl = '/api/authenticate';
$urlRouterProvider.otherwise('/login');
$stateProvider
.state('root', {
abstract: true,
url: '',
views: {
'layout': {
templateUrl: '../views/layout.html'
},
'header#root': {
templateUrl: '../views/shared/header.html'
},
'footer#root': {
templateUrl: '../views/shared/footer.html'
}
}
})
.state('root.login', {
url: '/login',
templateUrl: '../views/authView.html',
controller: 'AuthController as auth'
})
.state('root.account', {
url: '/account',
templateUrl: '../views/account.html',
controller: 'UserController as user'
})
.state('root.dashboard', {
url: '/dashboard',
templateUrl: '../views/dashboard.html',
controller: 'UserController as user'
})
.state('root.report', {
url: '/report',
templateUrl: '../views/account.html',
controller: 'UserController as user'
});
})
.run(function ($rootScope, $state, $location) {
// $stateChangeStart is fired whenever the state changes. We can use some parameters
// such as toState to hook into details about the state as it is changing
$rootScope.$on('$stateChangeStart', function (event, toState) {
// Grab the user from local storage and parse it to an object
var user = JSON.parse(localStorage.getItem('user'));
// If there is any user data in local storage then the user is quite
// likely authenticated. If their token is expired, or if they are
// otherwise not actually authenticated, they will be redirected to
// the auth state because of the rejected request anyway
if (user) {
// The user's authenticated state gets flipped to
// true so we can now show parts of the UI that rely
// on the user being logged in
$rootScope.authenticated = true;
// Putting the user's data on $rootScope allows
// us to access it anywhere across the app. Here
// we are grabbing what is in local storage
$rootScope.currentUser = user;
// If the user is logged in and we hit the auth route we don't need
// to stay there and can send the user to the main state
if (toState.name === "root.login") {
// Preventing the default behavior allows us to use $state.go
// to change states
event.preventDefault();
// go to the "main" state which in our case is dashboard
$state.go('root.dashboard');
}
}
});
});
i've added :
else{
// go to the "login" state
$state.go("root.login");
event.preventDefault();
}
in
if (user) {}

loading a state depending on local storage token property - stateProvider, ionic / angular js

I have this code:
var app = angular.module('myApp', ['ionic']);
app.config(function($stateProvider) {
$stateProvider
.state('login', {
url: '/',
templateUrl: 'login.html',
controller: 'loginController'
})
.state('home', {
url: '/home',
templateUrl: 'home.html',
controller: 'homeController'
});
});
$urlRouterProvider.otherwise('/');
How can I add a condition to state provider to verify if localstorage.token exist. If yes go to home else go to login
Now, I am going all the time on login state and there (loginController) I verify if I have or not a token on localstorage. I'm not satisfied with my version... that's why I want to improve it
I would recommend setting home as your default route and performing a redirect if there is no token. So this would be your default route:
$urlRouterProvider.otherwise('/home');
And you would redirect if there is no token. You can do that by watching for the $locationChangeStart event in your run block:
.run(function ($rootScope, $state) {
$rootScope.on('$locationChangeStart', function(event, next, current) {
// check for the user's token and that we aren't going to the login view
if(!localStorage.token && next.templateUrl != 'login.html'){
// go to the login view
$state.go('login');
}
}
})
This has the added benefit of restricting the user to the login view if they have not authenticated.
In app.run you can add your logic; this will fire on page-load and on refresh.
.run(function ($state) {
if(localStorage.token) $state.go('home');
else $state.go('login');
})

How to redirect users with unverified emails using Angular UI-Router?

I am using AngularJS with Meteor and wanted to redirect users with unverified emails to the sign in page. I have created a sign in view in /client/routes.js:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('signin', {
url:'/signin',
views: {
main: {
templateUrl: 'client/views/profile/signin.tpl'
}
}
})
Note that there are other states I am not listing for brevity sake.
Now, I want to redirect users to this sign in page if their emails have not been verified. How do I modify the example below from UI-Router FAQs to meet my needs? Other solutions not using the example below are acceptable to me as long as they address the issue at hand.
Example: Uses the data object on the state config to define a rule
function that will run logic against the user (here using an example
service called $currentUser). The $stateChangeStart handler catches
all state transition and performs this rule check before allowing the
transition, potentially blocking it and/or redirecting to a different
state.
app.config(function($stateProvider) {
$stateProvider.state('privatePage', {
data: {
rule: function(user) {
// ...
}
});
});
app.run(function($rootScope, $state, $currentUser) {
$rootScope.$on('$stateChangeStart', function(e, to) {
if (!angular.isFunction(to.data.rule)) return;
var result = to.data.rule($currentUser);
if (result && result.to) {
e.preventDefault();
// Optionally set option.notify to false if you don't want
// to retrigger another $stateChangeStart event
$state.go(result.to, result.params, {notify: false});
}
});
});
The example from the FAQ attempts to create a general way to add a rule to any page. Let's keep it simple:
app.run(function($rootScope, $state, UserService) {
$rootScope.$on('$stateChangeStart', function(event, toState) {
// don't check auth on login routes
if (["signin"].indexOf(toState.name) === -1) {
if (UserService.doesNotHaveVerifiedEmail()) {
event.preventDefault();
$state.go('signin');
return;
}
}
}
});
Anytime a state is loaded and it's not the signin state, you check if the user is verified (depends on your application, here I am injecting a UserService which I assume has knowledge about the user's status) and if not, you prevent that state change and redirect them to the signin page.
You can use the resolve functionality provided by angular-ui-router to check email verification of current user before the state is resolved. Here's how the code will look like:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
var isVerified = ['User', '$state', '$q',
function(User, $state, $q) {
var d = $q.defer();
var loginPromise = User.getVerificationStatus();
loginPromise.then(
function(response) {
d.resolve(response);
},
function(error) {
$state.go('login');
});
return d.promise;
}
];
$urlRouterProvider.otherwise('/');
$stateProvider
.state('signin', {
url: '/signin',
views: {
main: {
templateUrl: 'client/views/profile/signin.tpl'
}
}
})
.state('home', {
url: '/home',
views: {
main: {
templateUrl: 'client/views/profile/home.tpl'
}
},
resolve: {
verified: isVerified
}
});
}
]);
Here home state checks for the verification before resolving. I have injected a service User which will arrange the information whether user is verified or not.
You can add resolve property to only those states where you want to check verification status. In this way this is better than checking on $stateChangeStart event which will be fired every time state changes irrespective of whether this check is needed or not.
Here's the link to the documentation.

Angular UI Router: nested states for home to differentiate logged in and logged out

I'm starting a new project using boilerplate MEAN provided by MEAN.JS (not .IO).
I'm new to ui-router and I'm having trouble figuring out how to accomplish this scenario:
if user is logged in, go to state "home.loggedIn".
if user is logged out, go to state "home.loggedOut"
the route url is "/" and shouldn't change.
here's how the route provider looks like currently:
angular.module('core').config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
// Redirect to home view when route not found
$urlRouterProvider.otherwise('/');
// Home state routing
$stateProvider.
state('home', {
url: '/',
abstract: true
}).
state('home.loggedOut', {
templateUrl: 'modules/core/views/home.client.view.html'
}).
state('home.loggedIn', {
templateUrl: 'modules/core/views/dashboard.client.view.html'
});
}
]);
I'm looking for something like a pre-save hook in db terms to determine which state to go to. How would that look like?
There is a plunker providing behaviour as described above. Firstly, there is a change in a state definition, moving the url from abstract into both child states, and introducing logon state for later checks:
// Redirect to home view when route not found
$urlRouterProvider.otherwise('/');
// Home state routing
$stateProvider.
state('home', {
//url: '/',
abstract: true,
template: '<div ui-view=""></div>',
}).
state('home.loggedOut', {
url: '/',
...
}).
state('home.loggedIn', {
url: '/',
...
})
.state('logon', {
url: '/logon',
templateUrl: 'tpl.logon.html',
controller: 'LogonCtrl',
});
What we do have right now, is definition of 2 states, with a same url. The first will be taken as a default.. and used.
Now, we have to introduce a state change observer, which will redirect to proper sub-state, based on a AuthSvc setting isLoggedIn:
.run(['$rootScope', '$state', 'AuthSvc',
function($rootScope, $state, AuthSvc) {
$rootScope.$on('$stateChangeStart', function(event, toState, toParams
, fromState, fromParams) {
// logged out is logged out
var doRedirectToLoggedOut = !AuthSvc.isLoggedIn
&& toState.name === "home.loggedIn";
if (doRedirectToLoggedOut) {
event.preventDefault();
$state.go("home.loggedOut");
}
// logged in is logged in
var doRedirectToLoggedIn = AuthSvc.isLoggedIn
&& toState.name === "home.loggedOut";
if (doRedirectToLoggedIn) {
event.preventDefault();
$state.go("home.loggedIn");
}
});
}])
As this example shows in action, until we change isLoggedIn (click on logon link) we are redirected to correct sub-state ... even if we would like to see the other

Resources