An AngularJS service is injected into two separate modules. This is causing the service to re-initialize separately when the second module calls it. I have used the FireFox debugger to confirm that the module is being re-initialized. How can I avoid this problem?
Here is the specific case:
An AngularJS app uses an authentication service in a module called auth to manage authentication concerns. The auth service is imported into a message module which manages access to the secure /message route and auth is also imported into a navigation module which manages both login/registration and also the visible contents of the navigation links in the user's browser. A user is able to successfully login using the GUI tools linked to the navigation module, and is then successfully redirected to the secure /message route as an authenticated user because the auth.authenticated1 and auth.authenticated2 properties are set to true just before the redirect to /message occurs.
However, the FireFox debugger confirms that the problem is that, when the user refreshes the browser to reload the /message url pattern, the auth module is re-initialized, setting the values of auth.authenticated1 and auth.authenticated2 back to false, and thus giving the user a message that they are not logged in, even though they were logged in a moment before using valid credentials provided by the user. What specific changes need to be made to the code below so that the user is NOT logged out on page re-load?
I want the AngularJS code to check the pre-existing value for auth.authenticated2 when the /message route is loaded or reloaded. If auth.authenticated2=false, then the user gets a message saying they are logged out. But if auth.authenticated2=true, I want the user to be able to see the secure content at the /message route. I DO NOT want auth.authenticated2 to be automatically re-set to false upon reloading the route, the way it is now.
Here is the code in message.html which contains the GUI elements for the /message route:
<div ng-show="authenticated2()">
<h1>Secure Content</h1>
<div>
<p>Secure content served up from the server using REST apis for authenticated users.</p>
</div>
</div>
<div ng-show="!authenticated2()">
<h1>You are not logged in.</h1>
</div>
Here is the code in message.js which is the controller for the message module that manages the /message route:
angular.module('message', ['auth']).controller('message', function($scope, $http, $sce, auth) {
$scope.authenticated2 = function() {
return auth.authenticated2;
}
//Other code calling REST apis from the server omitted here to stay on topic
});
Here is the code for the navigation module, which also injects the auth service:
angular.module('navigation', ['ngRoute', 'auth']).controller('navigation', function($scope, $route, auth, $http, $routeParams, $location) {
$scope.credentials = {};//from old navigation module
$scope.leadresult = "blank";
$scope.processStep = "start";
$scope.uname = "blank";
$scope.wleadid = "initial blank value";
$scope.existing = "blank";
$scope.tab = function(route) {
return $route.current && route === $route.current.controller;
};
$scope.authenticated1 = function() {
return auth.authenticated1;
}
$scope.authenticated2 = function() {
return auth.authenticated2;
}
$scope.login = function() {
auth.authenticate1($scope.credentials, function(authenticated1) {
//a bunch of stuff that does level 1 authentication, which is not relevant here
})
}
$scope.logout = auth.clear;
//some other methods to manage registration forms in a user registration process, which are omitted here because they are off-topic
$scope.pinForm = function(isValid) {//this method finishes authentication of user at login
if (isValid) {
$scope.resultmessage.webleadid = $scope.wleadid;
$scope.resultmessage.name = $scope.uname;
$scope.resultmessage.existing = $scope.existing;
var funcJSON = $scope.resultmessage;
auth.authenticate2(funcJSON, function(authenticated2) {
if (authenticated2) {
$location.path('/message');
$scope.$apply();//this line successfully re-directs user to `/message` route LOGGED IN with valid credentials
}
});
}
};
$scope.$on('$viewContentLoaded', function() {
//method that makes an unrelated call to a REST service for ANONYMOUS users
});
});
Here is the code for the auth service in auth.js:
angular.module('auth', []).factory( 'auth', function($rootScope, $http, $location) {
var auth = {
authenticated1 : false,
authenticated2 : false,
usrname : '',
loginPath : '/login',
logoutPath : '/logout',
homePath : '/message',
path : $location.path(),
authenticate1 : function(credentials, callback) {
var headers = credentials && credentials.username ? {
authorization : "Basic " + btoa(credentials.username + ":" + credentials.password)
} : {};
$http.get('user', {
headers : headers
}).success(function(data) {
if (data.name) { auth.authenticated1 = true; }
else { auth.authenticated1 = false; }
callback && callback(auth.authenticated1);
}).error(function() {
auth.authenticated1 = false;
callback && callback(false);
});
},
authenticate2 : function(funcJSON, callback) {
$http.post('/check-pin', funcJSON).then(function(response) {
if(response.data.content=='pinsuccess'){
auth.authenticated2=true;
callback && callback(auth.authenticated2);
}else {
auth.authenticated2=false;
auth.authenticated2 = false;
callback && callback(false);
}
});
},
clear : function() {
$location.path(auth.loginPath);
auth.authenticated1 = false;
auth.authenticated2 = false;
$http.post(auth.logoutPath, {}).success(function() { console.log("Logout succeeded");
}).error(function(data) { console.log("Logout failed"); });
},
init : function(homePath, loginPath, logoutPath) {
auth.homePath = homePath;
auth.loginPath = loginPath;
auth.logoutPath = logoutPath;
}
};
return auth;
});
The routeProvider is managed by the main js file for the app, which is hello.js and is as follows:
angular.module('hello', [ 'ngRoute', 'auth', 'home', 'message', 'public1', 'navigation' ])
.config(
function($routeProvider, $httpProvider, $locationProvider) {
$locationProvider.html5Mode(true);/* This line is one of 3 places where we set natural urls to remote the default # */
$routeProvider.when('/', {
templateUrl : 'js/home/home.html',
controller : 'home'
}).when('/message', {
templateUrl : 'js/message/message.html',
controller : 'message'
}).when('/public1', {
templateUrl : 'js/public1/public1.html',
controller : 'public1'
}).when('/register', {
templateUrl : 'js/navigation/register.html',
controller : 'navigation'
}).otherwise('/');
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
}).run(function(auth) {
// Initialize auth module with the home page and login/logout path
// respectively
auth.init('/checkpin', '/login', '/logout');
});
Not a complete answer as in, exact code to change here. But enough to get something built nicely.
You shouldn't explicitly use only boolean values inside your auth service. As far as I can see, you're not using any data that you're receiving from the server after a successful authentication. So whenever you refresh, everything is lost. You don't have anything in your code to recover from a refresh.
Ideally, you should have something like a token or a cookie. The cookie will be saved and can be recovered after a refresh, so while starting the auth service, you can check for the existence of that cookie.
You could also save a token that can be used to access an API inside an indexedDB or something like that. Just as I said before, during boot time of the auth service, you'll have to check for the existence of that token.
If you need more information, check for Oauth2. Even though Oauth2 isn't an Authentication API but an authorization API, the password grant type is something you could use. To get an idea how to build a solid system. The other grant types are mostly only focused on the authorization side of OAuth.
Because OP ask for code here it is:
when creating_service:
if exists(cookie) or exists(token) and valid(cookie) or valid(token):
mark_service_as_logged_in()
If pseudo code is better than words.
Related
I'm writing a website which uses ngRoute for changing pages.
for logging in a form will appear and when it's successful controller changes the http header for requests in the next steps.
bud the problem is that when I change the header, page should be reloaded if not, the Token would not be added to header.
Controller :
app.controller('catCtrl',['Api','$scope','$cookieStore','$rootScope',function (Api,$scope,$cookieStore,$rootScope) {
$scope.Login = function(){
Api.loginEmail($scope.log_email, $scope.pass, 'chrome', 'windows','').success(function(response){
$cookieStore.put('Auth-Key', 'Token ' + response.token);
$scope.is_Loggedin = true;
$scope.showLoginWin();
}).error(function(response){
$scope.log_email = null;
$scope.pass = null;
$scope.error = response.error;
});
};
}
App.run :
app.run(['$cookieStore','$http',function($cookieStore, $http){
$http.defaults.headers.common['Authorization'] = $cookieStore.get('Auth-Key');
}]);
How can I change the header without reloading page.
so you want to add your token on further request after login.
You can try angular interceptor. Here is few Answers related how to add toke via interceptor.
Interceptor Example 1
Interceptor example 2
sample Code:
app.factory('httpRequestInterceptor', function () {
return {
request: function (config) {
config.headers['Authorization'] = $cookieStore.get('Auth-Key');
return config;
}
};
});
In your service layer, Ignore verification this header for Login.
In my angular application I store the logged in user name in a cookie.
And I want to display this user name in the web page header.
I can retrieve this in my controller as below and display in the page.
$rootScope.fullName = $cookies.get('user');
But instead of doing this in every controller, is it possible to do it in one place and always get this data ?
Update #1:
I don't use ui-view. I use angular route and a sample is as below.
Are there any simple approach please ?
var mdmApp = angular.module('mdmApp');
// Routes
mdmApp.config(function($routeProvider, $httpProvider, $locationProvider) {
$routeProvider
.when('/listScopeAndFrequency/:reportTypeId', {
templateUrl : '3_calendar/listScopeAndFrequencies.html',
controller : "listScopeAndFrequenciesController"
})
.when('/listTemplateFrequencyExceptions/:reportTypeId/:consolidationScopeCode/:frequencyCode', {
templateUrl : '3_calendar/listTemplateFrequencyExceptions.html',
controller : "listTemplateFrequencyExceptionsController"
})
.when('/viewSubmissionDates', {
templateUrl : '3_calendar/viewSubmissionDates.html',
controller : "viewSubmissionDatesController"
})
Update #2
I tried like below but could not get any dynamic values from REST API, I am only able to hard code the value. Not able to read from cookie, localStorage or var all are undefined or give errors.
mdmApp.factory('userService', function($http, $localStorage, $cookies) {
var fullName ;
// Gets user details
$http.get("/mdm/getUser")
.success(function(data, status, headers, config) {
console.log('userService > user : ' +data.fullName);
fullName = data.fullName;
$localStorage.user = data;
$cookies.put('user', data.fullName);
})
.error(function(data, status, headers, config, statusText) {
console.log("Error while retrieving user details.");
});
console.log('fullName : ' +fullName);
console.log('$cookies.get(user) : ' +$cookies.get('user'));
console.log('$localStorage.user : ' +$localStorage.user);
return { name: "hard code only works here"};
});
mdmApp.directive('userTitle', ['userService', function(user) {
console.log('userTitle > user : ' +user);
return {
template: user.name,
};
}]);
Why don't you use a service? Those are specially designed for this task:
Angular services are:
Lazily instantiated – Angular only instantiates a service when an application component depends on it.
Singletons – Each component dependent on a service gets a reference to the single instance generated by the service factory.
How I do it (not necessarily the best way):
setup an independent angular app (non-secured area) for user registration, on login store a token or some identifier in a cookie or the localStorage
when instantiating the full angular app (secured area), create a service which will get back the previous value and inject it whenever you need the user
My User service looks like:
angular.module('app.core')
.service('User', User);
function User($rootScope, localStorageService) {
let user = localStorageService.get('user');
let token = localStorageService.get('userToken');
if(!token) {
//this loads another angular app which has nothing to do with this secured area (on /login)
window.location = '/login'
return;
}
for(let i in user)
this[i] = user[i]
this.token = 'Bearer ' + token;
//note you could reference this to the rootScope so that every scope can have the User object, this can be considered as bad practice!
//$rootScope.user = this;
return this;
}
I would go with a service and a directory for this.
As soyuka explained. Services exists for the purpose of sharing data between parts of an application. I would then use a directive to handle the data in relation to the DOM, that is, to write to the header.
Service/factory:
app.factory('userService', function() {
// do what you have to get your user and save the data.
return { name: 'John'};
});
Directive. Notice the injection of the service. This could be done through a controller if you wish. It depends on whether you want your directives to have a dependency to the service.
app.directive('userTitle', ['userService', function(user) {
// do what you want with the data
return {
template: user.name,
};
}]);
Here the service just returns a dummy name and the directive outputs the name in the DOM.
Here is a running plunker of the setup.
I have a single page angularjs app in which whenever there is a change in route I check the login status of the user by a variable stored in a service(after submitting the login form to server) as per this solution AngularJS- Login and Authentication in each route and controller:
app.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
$rootScope.$on('$routeChangeStart', function (event) {
if (!Auth.isLoggedIn()) {
console.log('DENY');
event.preventDefault();
$location.path('/login');
}
else {
console.log('ALLOW');
$location.path('/home');
}
});}]);
//service
.factory('Auth', function(){
var user;
return{
setUser : function(aUser){
user = aUser;
},
isLoggedIn : function(){
return(user)? user : false;
}
}
})
The problem is when I reload the whole page(by the refresh button) the variable in the service is lost and user gets redirected to login page even when the user session is still on at the backend.
How can I still manage the variable in the service? I thought of using sessionStorage but does not sound secure enough.
Im my opinion you can choose from 2 ways:
Persist the Data on the server-side via session
Store your data in the localStorage or even better in the window.sessionStorage so a page reload doesn't affect your applicant
Maybe angular-cookies can solve your problem
Try to store your data in $window.localStorage ( angular abstraction over javascript window.localStorage)
for example :
(function () {
angular.module('app').factory('UserIdentity', ['$window', function ($window) {
this.UserName = function () {
return $window.localStorage.getItem("username");
};
this.Token = function () {
return $window.localStorage.getItem("token");
};
this.create = function (token, userName) {
$window.localStorage.setItem("token", token);
$window.localStorage.setItem("username", userName);
};
this.destroy = function () {
$window.localStorage.removeItem("token");
$window.localStorage.removeItem("username");
};
this.isAuthenticated = function () {
var token = $window.localStorage.getItem("token");
return !!token;
}
return this;
}]);
})();
So let's say I have the following user property and I want to restrict access to a page. This is in my Firebase noSQL database but I think this could pertain to obtaining data from anywhere.
{
"users": {
"simplelogin:1": {
"properties": { "admin_user": true }
}
}
}
So in my javascript I have the following:
var user_properties = new Firebase("https://<MY-URL>.com/users/"+auth.uid+"/properties");
user_properties.once("value", function(properties) {
if(properties.val().admin_user == false)
window.location.replace("/");
});
So, on the page load of the "admin page", I load this javascript. And if they aren't an admin, the page is supposed to redirect.
However, I'm having the problem where the admin page will load for a second while it gathers the data and then redirect.
Does anyone have any suggestions on how I can make the page redirect before the page even loads?
Security rules in Firebase can prevent the page from data from being viewed without permission. Then you simply need a client-side solution to redirect the page. The simple answer here is to use resolve in your routes.
You can find a complete implementation of this approach in the angularFire-seed project. Here's the relevant code:
"use strict";
angular.module('myApp.routes', ['ngRoute', 'simpleLogin'])
.constant('ROUTES', {
'/home': {
templateUrl: 'partials/home.html',
controller: 'HomeCtrl',
resolve: {
// forces the page to wait for this promise to resolve before controller is loaded
// the controller can then inject `user` as a dependency. This could also be done
// in the controller, but this makes things cleaner (controller doesn't need to worry
// about auth status or timing of displaying its UI components)
user: ['simpleLogin', function(simpleLogin) {
return simpleLogin.getUser();
}]
}
},
'/chat': {
templateUrl: 'partials/chat.html',
controller: 'ChatCtrl'
},
'/login': {
templateUrl: 'partials/login.html',
controller: 'LoginCtrl'
},
'/account': {
templateUrl: 'partials/account.html',
controller: 'AccountCtrl',
// require user to be logged in to view this route
// the whenAuthenticated method below will resolve the current user
// before this controller loads and redirect if necessary
authRequired: true
}
})
/**
* Adds a special `whenAuthenticated` method onto $routeProvider. This special method,
* when called, invokes the requireUser() service (see simpleLogin.js).
*
* The promise either resolves to the authenticated user object and makes it available to
* dependency injection (see AuthCtrl), or rejects the promise if user is not logged in,
* forcing a redirect to the /login page
*/
.config(['$routeProvider', function($routeProvider) {
// credits for this idea: https://groups.google.com/forum/#!msg/angular/dPr9BpIZID0/MgWVluo_Tg8J
// unfortunately, a decorator cannot be use here because they are not applied until after
// the .config calls resolve, so they can't be used during route configuration, so we have
// to hack it directly onto the $routeProvider object
$routeProvider.whenAuthenticated = function(path, route) {
route.resolve = route.resolve || {};
route.resolve.user = ['requireUser', function(requireUser) {
return requireUser();
}];
$routeProvider.when(path, route);
}
}])
// configure views; the authRequired parameter is used for specifying pages
// which should only be available while logged in
.config(['$routeProvider', 'ROUTES', function($routeProvider, ROUTES) {
angular.forEach(ROUTES, function(route, path) {
if( route.authRequired ) {
// adds a {resolve: user: {...}} promise which is rejected if
// the user is not authenticated or fulfills with the user object
// on success (the user object is then available to dependency injection)
$routeProvider.whenAuthenticated(path, route);
}
else {
// all other routes are added normally
$routeProvider.when(path, route);
}
});
// routes which are not in our map are redirected to /home
$routeProvider.otherwise({redirectTo: '/home'});
}])
/**
* Apply some route security. Any route's resolve method can reject the promise with
* { authRequired: true } to force a redirect. This method enforces that and also watches
* for changes in auth status which might require us to navigate away from a path
* that we can no longer view.
*/
.run(['$rootScope', '$location', 'simpleLogin', 'ROUTES', 'loginRedirectPath',
function($rootScope, $location, simpleLogin, ROUTES, loginRedirectPath) {
// watch for login status changes and redirect if appropriate
simpleLogin.watch(check, $rootScope);
// some of our routes may reject resolve promises with the special {authRequired: true} error
// this redirects to the login page whenever that is encountered
$rootScope.$on("$routeChangeError", function(e, next, prev, err) {
if( angular.isObject(err) && err.authRequired ) {
$location.path(loginRedirectPath);
}
});
function check(user) {
if( !user && authRequired($location.path()) ) {
$location.path(loginRedirectPath);
}
}
function authRequired(path) {
return ROUTES.hasOwnProperty(path) && ROUTES[path].authRequired;
}
}
]);
My angular app (SPA) needs to redirect to another server if user is not authenticated. This is a separate machine which means there can be a delay when redirecting from the my angular app to this auth server.
What I am looking to achieve is as follows:
When the app is requested and being loaded , it either dont show the content or show a vanilla/simple page.
If the app finds that the user is not logged in or login expired then it will continue to show that vanilla page while redirecting the app to this auth server.
Would really appreciate inputs in this.
Thanks,
Edit: interceptor.js code looks as follows:
app.factory('authInterceptorService', ['$q', '$injector', '$location', 'localStorageService',
function ($q, $injector, $location, localStorageService) {
....
var request = function (config) {
config.headers = config.headers || {};
var fragments = getFragment();
if(fragments.access_token != undefined)
localStorageService.add("authorizationData", { token: fragments.access_token, token_type: fragments.token_type, state : fragments.state, });
var authData = localStorageService.get('authorizationData');
if(authData)
{
config.headers.Authorization = authData.token_type + ' ' + authData.token;
$location.path( "/dashboard" );
}
else
logout();
return config;
};
var responseError = function (rejection) {
if (rejection.status === 401) {
logout();
}
return $q.reject(rejection);
};
var logout = function()
{
localStorageService.remove('authorizationData');
var scope = 'my-scope';
var uri = addQueryString('http://www.authserver.com/OAuth/Authorize', {
'client_id': 'dsfdsafdsafdsfdsfdsafasdf',
'redirect_uri': 'www.returnuri.com',
'state': 'asdfasfsdf',
'scope': 'scope1',
'response_type': 'token'
});
window.location.replace(uri);
};
authInterceptorServiceFactory.request = request;
authInterceptorServiceFactory.responseError = responseError;
authInterceptorServiceFactory.logout = logout;
return authInterceptorServiceFactory;
}]);
});
which is similar to what is being suggested by Blackunknown. But the problem is that the landing page gets loaded fully and then its gets redirected to the auth server. I know that the issue is that they are separate servers so they can have different response time.
I use a couple of things to get this done in an mvc 5 application. Of which the most important component being the AuthorizeInterceptor. I use a class set up in my coffee/javascripts than you will be seeing in most examples but the main principles are the same. I'll spare you the coffee here is some javascript:
(function() {
"use strict";
var AuthorizeConfig, AuthorizeInterceptor;
AuthorizeInterceptor = (function() {
function AuthorizeInterceptor($q, $location) {
this.$q = $q;
this.$location = $location;
return {
response: function(response) {
return response || $q.when(response);
},
responseError: function(rejection) {
if (((rejection != null ? rejection.status : void 0) != null) && rejection.status === 401) {
$location.path("/Login");
}
return $q.reject(rejection);
}
};
}
return AuthorizeInterceptor;
})();
angular.module("myapp").factory("AuthorizeInterceptor", ["$q", "$location", AuthorizeInterceptor]);
AuthorizeConfig = (function() {
function AuthorizeConfig($httpProvider) {
$httpProvider.interceptors.push("AuthorizeInterceptor");
}
return AuthorizeConfig;
})();
angular.module("myapp").config(["$httpProvider", AuthorizeConfig]);
}).call(this);
When a request results in a 401 it will redirect this person to the login page of the application.
Since you provided absolutely no code, here's a pseudo-example:
$http.get('yourAuthServer').success(function(response){
// save session data and redirect the user to the regular page
$location.path('loggedInRoute');
}).error(function(err){
// handle the failed authentification here
$location.path('authFailed');
});
So, the idea is to have a landing page with no sensitive data. You'll make an auth request from the main controller and, based on the results, you'll redirect the user properly. Of course, you should have authentication checks in place on your logged in page and not rely only on that redirection. But that will get you started.