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;
}]);
})();
Related
I am currently using $rootScope to store user information and whether or not the user is logged in. I have tried using $window.localStorage, but with no success. My goal is to have items in my navbar appear through an ng-show once a user is logged on, have their username appear in the navbar, individual user profile view, all users view, etc. I need a persistent login. I have the navbar working with $rootscope, but whenever I try and transition over to $window.localStorage, it fails. Here is the code using $rootScope:
mainModule
angular.module('mainModule', [
'ui.router',
...
])
.config(configFunction)
.run(['$rootScope', '$state', 'Auth', function($rootScope, $state, Auth) {
$rootScope.$on('$stateChangeStart', function(event, next) {
if (next.requireAuth && !Auth.getAuthStatus()) {
console.log('DENY');
event.preventDefault();
$state.go('login');
} else if (Auth.getAuthStatus() || !Auth.getAuthStatus()) {
console.log('ALLOW');
}
});
}]);
Auth Factory
angular.module('authModule').factory('Auth', ['$http', '$state', function authFactory($http, $state) {
var factory = {};
var loggedIn = false;
var userData = {};
factory.getAuthStatus = function() {
$http.get('/api/v1/auth')
.success(function(data) {
if (data.status == true) {
loggedIn = true;
} else {
loggedIn = false;
}
})
.error(function(error) {
console.log(error);
loggedIn = false;
});
return loggedIn;
}
return factory;
}]);
Login Controller
function SigninController($scope, $rootScope, $http, $state) {
$scope.userData = {};
$scope.loginUser = function() {
$http.post('api/v1/login', $scope.userData)
.success((data) => {
$scope.userData = data.data;
$rootScope.loggedIn = true;
$rootScope.userData = data;
$state.go('home');
})
.error((error) => {
console.log('Error: ' + error);
});
};
}
Nav Controller
function NavbarController($scope, Auth) {
$scope.loggedIn = Auth.getAuthStatus();
}
EDIT EDIT EDIT
Here is how I am using local storage. These are the only things that changed.
Login Controller
function SigninController($scope, $window, $http, $state) {
$scope.userData = {};
$scope.loginUser = function() {
$http.post('api/v1/login', $scope.userData)
.success((data) => {
$scope.userData = data.data;
$window.localStorage.setItem('userData', angular.toJson(data));
$window.localStorage.setItem('loggedIn', true);
$state.go('home');
})
.error((error) => {
console.log('Error: ' + error);
});
};
}
Auth Factory
angular
.module('authModule')
.factory('Auth', ['$http', '$window', '$state', function authFactory($http, $window, $state) {
var factory = {};
factory.getAuthStatus = function() {
$http.get('/api/v1/auth')
.success(function(data) {
if (data.status == true) {
$window.localStorage.setItem('loggedIn', true);
} else {
$window.localStorage.setItem('loggedIn', false);
}
})
.error(function(error) {
console.log(error);
$window.localStorage.setItem('loggedIn', false);
});
return $window.localStorage.getItem('loggedIn');
}
return factory;
}]);
I see a potential problem with your use of localStorage.getItem('loggedIn').
Because localStorage only stores strings, what you get back is actually a stringified version of the boolean that you put in. If the string 'false' gets returned, your check of !Auth.getAuthStatus() in main module for example will always evaluate to boolean false because any non-empty string in JavaScript is "truthy".
i.e. !'false' === false (the same as !true === false)
You can get over this by using JSON.parse on the value in localStorage. e.g. JSON.parse(localStorage.getItem('loggedIn')) would parse the string 'false' to the Boolean false.
Simply replace $window.localStorage with window.localStorage and you should be fine.
For example:
function SigninController($scope, $window, $http, $state) {
$scope.userData = {};
$scope.loginUser = function() {
$http.post('api/v1/login', $scope.userData)
.success((data) => {
$scope.userData = data.data;
window.localStorage.setItem('userData', angular.toJson(data));
window.localStorage.setItem('loggedIn', true);
$state.go('home');
})
.error((error) => {
console.log('Error: ' + error);
});
};
}
This being said, storing authenticated status in localStorage (or sessionStorage) is not a good path to go down. Both key/value pairs can be read in the developer pane and then altered (aka spoofed) via the console. A better solution is to return a unique value (GUID) after a successful login and store it in a cookie (set to expire in a short amount of time, like 20 minutes) that can be read on the server and verified there. You can and should use $cookie for this. Your user login state should be controlled server-side, never client-side. The client should always have to prove that it is authenticated.
To persist login, create a service that handles your visitor and let that service handle the login/logout and provide the proof of being logged in. That proof of being logged in should always be a private value that is held internally by the service and not accessible outside of it.
(function () {
'use strict';
var visitorModelService = ['$http', function ($http) {
var loggedIn = false,
visitorModel = {
login:function(){
//do login stuff with $http here
//set loggedIn to true upon success
},
loggedIn:function(){
return loggedIn;
},
logout:function(){
//do logout stuff with $http here
//no matter what, set loggedIn to false
}
};
return visitorModel;
}];
var module = angular.module('models.VisitorModel', []);
module.factory('VisitorModel', visitorModelService);
}());
Doing this, you can simply check for visitor.loggedIn in your ng-show and have everything centralized. Such as:
<a ng-click='visitor.logout' ng-show='visitor.loggedIn'>Log Out</a>
Better yet, put the elements that are only visible to authenticated users in a div tag and hide/show them en-mass.
What I am looking to do I when a user comes to the index.html page I need the login module to do 2 things:
I need this to check if a user is authenticated ( which I think I already started with the "function authService" ) if the user has a valid token then change the ui-view to dashboard/dashboard.html and if the key is not valid or there is no key at all then load login/login.html into ui-view.
Once they have successfully logged in I want them to be routed to "dashboard/dashboard.html"
Here is my login script:
function authInterceptor(API) {
return {
request: function(config) {
if(config.url.indexOf(API) === 0) {
request.headers = request.headers || {};
request.headers['X-PCC-API-TOKEN'] = localStorage.getItem('token');
}
return config;
}
}
}
function authService(auth) {
var self = this;
self.isAuthed = function() {
localStorage.getItem('token');
}
}
function userService($http, API) {
$http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;';
$http.defaults.transformRequest = [function(data) {
return angular.isObject(data) && String(data) !== '[object File]' ? param(data) : data;
}];
var self = this;
self.login = function(username, pwd, ctrl) {
ctrl.requestdata = API + '/winauth' + '; with ' + username;
return $http.post(API + '/winauth', {
username: username,
pwd: pwd
})
};
var param = function(obj) {
var query = '', name, value, fullSubName, subName, subValue, innerObj, i;
for(name in obj) {
value = obj[name];
if(value instanceof Array) {
for(i=0; i<value.length; ++i) {
subValue = value[i];
fullSubName = name + '[' + i + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += param(innerObj) + '&';
}
}
else if(value instanceof Object) {
for(subName in value) {
subValue = value[subName];
fullSubName = name + '[' + subName + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += param(innerObj) + '&';
}
}
else if(value !== undefined && value !== null)
query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';
}
return query.length ? query.substr(0, query.length - 1) : query;
};
}
function LoginCtrl(user) {
var self = this;
function handleRequest(res) {
self.responsedata = res;
self.message = res.data.message;
var authToken = res.data.auth_token;
localStorage.setItem('token', authToken);
}
self.login = function() {
this.requestdata = 'Starting request...';
user.login(self.username, self.pwd, self)
.then(handleRequest, handleRequest)
}
}
// Login Module
var login = angular.module('login', ["ui.router"])
login.factory('authInterceptor', authInterceptor)
login.service('user', userService)
login.service('auth', authService)
login.constant('API', 'http://myserver.com/api')
EDIT - I added this into my login controller to provide the login routes
login.config(function($httpProvider, $stateProvider, $urlRouterProvider) {
$httpProvider.interceptors.push('authInterceptor');
$urlRouterProvider.otherwise('/login');
$stateProvider
// HOME STATES AND NESTED VIEWS ========================================
.state('login', {
url: '/login',
templateUrl: 'login/login.html',
controller: "mainLogin",
controllerAs: "log"
})
// nested list with just some random string data
.state('dashboard', {
url: '/dashboard',
templateUrl: 'dashboard/dashboard.html',
})
})
login.controller('mainLogin', LoginCtrl)
Here is my index.html:
EDIT - I removed "ng-include" and added "ng-view" to control the routes.
<body ng-app="login" ng-controller="mainLogin as log" class="loginPage">
<div class="main" ui-view></div>
</body>
As you can see I have a function that is checking for the token in the users local storage:
function authService(auth) {
var self = this;
self.isAuthed = function() {
localStorage.getItem('token');
}
}
And I am loading it in the module as a service:
login.service('auth', authService)
This is where I am stuck. I don't know where to go from here. I don't even know if I am using my authService function properly. I am still learning a lot about AngularJS so its easy for me to get stuck. :)
Another thing you will notice is in my index.html file I am just loading the "login/login.html" partial as default. I need it to load either login.html or dashboard.html depending if they are logged in or not. And then also route them to dashboard.html once they have successfully logged in.
The script works great as far as hitting the auth API, authenticating the user and then storing a valid auth key on their local storage.
Anyone know how I can accomplish this?
There are two separate concerns that you are dealing with. The first, is to be able to determine if you are logged in. Assuming the user needs to be logged in for any state except the login state, you would implement it like so by listening for $stateChangeState events and verifying that the user is logged in:
login.run(function($state, authService) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
var authToken = authService.isAuthed();
if (!authToken && toState !== 'login') {
//not logged in, so redirect to the login view instead of the view
//the user was attempting to load
event.preventDefault();
$state.go('login');
}
})
});
This will put them on the login state if they haven't already logged in.
The second part is to redirect to the correct view after they login, which you would do in your login controller:
function LoginCtrl(user, $state) {
var self = this;
function handleRequest(res) {
self.responsedata = res;
self.message = res.data.message;
var authToken = res.data.auth_token;
localStorage.setItem('token', authToken);
//after successful login, redirect to dashboard
$state.go('dashboard');
}
self.login = function() {
this.requestdata = 'Starting request...';
user.login(self.username, self.pwd, self)
.then(handleRequest, handleRequest)
}
}
ok I see you are using ui.router so let's work within this framework.
You want to
check if a user is logged in
redirect user to a view
What you're looking for is resolve:{loggedIn: checkLoggedInFn}
so your route for dashboard could be something like
.state('dashboard', {
url: '/dashboard',
templateUrl: 'dashboard/dashboard.html',
resolve: {
loggedIn: function(){
//do your checking here
}
}
})
what this does basically is that the controller will not instantiate until every resolve is resolved (so you can use a promise here for example), and then the value is passed into the controller as a parameter, so you could then do something like:
if(!loggedIn){
$state.go('login');
}
You would handle the logic inside your login controller specifically here:
self.login = function() {
this.requestdata = 'Starting request...';
user.login(self.username, self.pwd, self)
.then(handleRequest, handleRequest)
}
Inside the callback for the success on login, you would simple do a state change to send the user to the correct state.
In authInterceptor add code for response. So:
return {
request: function(config) {
if(config.url.indexOf(API) === 0) {
request.headers = request.headers || {};
request.headers['X-PCC-API-TOKEN'] = localStorage.getItem('token');
}
return config;
},
response: function(response) {
//ok response - code 200
return response;
},
responseError: function(response){
//wrong response - different response code
}
};
On server side check http header X-PCC-API-TOKEN and if is wrong ( no authentication) response should have different code like 403. So responseError method will run in interceptor.
responseError: function(response){
//wrong response - different response code
if (response.status === 403) {
alert("No rights to page");//your code for no auth
//redirect to different route
$injector.get('$state').transitionTo('login');//use injector service
return $q.reject(response);//return rejection - use $q
}
}
Your service is fine and it's on the loginModule but you are not using it anywhere where i can see. You need to inject your service into controller to do stuff you want. In your authService you are getting item from localstorage but you are not returning anything for example you have your login service
function authService(auth) {
var self = this;
self.isAuthed = function() {
return localStorage.getItem('token');
}
}
//here you can inject your auth service to get it work as you want
function LoginCtrl(user, auth) {
var self = this;
function handleRequest(res) {
self.responsedata = res;
self.message = res.data.message;
var authToken = res.data.auth_token;
localStorage.setItem('token', authToken);
}
self.login = function() {
this.requestdata = 'Starting request...';
user.login(self.username, self.pwd, self)
.then(handleRequest, handleRequest)
}
}
login.service('auth', authService)
function authService(auth) {
var self = this;
self.isAuthed = function() {
**localStorage.getItem('token');**
}
}
Where are you getting the localstorage item into? The LValue is missing.
At the most basic level, you could handle a check for this item - token - in the Dashboard page, at the time of loading the page and if it is null ie. empty, then redirect/route the user to the login page. Btw, use the sessionStorage rather than the localStorage as the former will flush as soon as the browser session is closed.
There are more elegant and simpler ways of accomplishing it like Passport. Have you checked it? It is as simple as this:
app.post('/login', passport.authenticate('local', { successRedirect: '/',
failureRedirect:'/login'}));
Your code isn't checking on url changes or affecting routes in a cross-cutting way.
Remember that authentication and authorization are cross-cutting concerns. That being said, Angular has a way for you to intercept routing calls by listening on $routeChangeStart. Your "interceptor" should be added there. You can then redirect the router to the required view by manually routing there. Have a look as the solution from a previous stack overflow thread.
There is a simple way you can achieve what you want for your application, using PassportJs.
The documentation is pretty simple and easy to implement.
You can also refer this tutorial to implement authentication using Passport. This tutorial teaches in very simple way, how to do authentication for your application.
Simple way to do that is just use https://github.com/Emallates/ng-enoa-auth package. You just need to include it in your app, nothing else.
I'm trying to display an avatar of an user once the user logs in:
<img src="{{(API_PROVIDER.domain + user.avatar.small_thumb.url)}}" alt="" class="img-circle size-30x30">
But the above code only works if I reload the page after login. How can I get it to work without having to programmatically reload the page?
PS: The above resolves to something like this: www.example.com/api/something.jpg
EDIT:
I have tried using ng-src instead of src and it didn't work. As to the other comment whether my variables were in scope, yes, the avatar link is only defined when the user signs in. Then I use $state.go('somewhere') to change the template, in which case I'd image the variable should be updated.
Here's my main controller:
(function() {
'use strict';
angular
.module('admin')
.controller('MainController', MainController);
/** #ngInject */
function MainController($timeout, webDevTec, toastr, $scope, $http, authenticatedUser, Session, $anchorScroll, API_PROVIDER) {
...
$scope.session = Session;
$scope.user = Session.user;
$scope.API_PROVIDER = API_PROVIDER;
...
}
})();
Here ar the components of my Session (reduced for brevity):
...
this.create = function(user) {
this.user = user;
this.role = user._role;
this.token = user.auth_token;
this.userRole = user._role;
};
return this;
...
And how the session is saved for later retrieval:
...
$window.sessionStorage["userInfo"] = JSON.stringify(loginData);
...
Do I need to use $apply() in this case? If yes, how so?
EDIT 3: Here's how I'm setting my Session object
authService.login = function(user, success, error, $state) {
$http.post(API_PROVIDER.full_path + 'signin', user).success(function(data) {
if(data.success){
var user = data.user;
var loginData = user;
$window.sessionStorage["userInfo"] = JSON.stringify(loginData);
delete loginData.password;
Session.create(loginData);
$rootScope.$broadcast(AUTH_EVENTS.loginSuccess);
success(loginData);
} else {
$rootScope.$broadcast(AUTH_EVENTS.loginFailed);
error();
}
});
};
Force reload images
https://stackoverflow.com/a/21731946/2906183
Apply time stamp and call $scope.$appy()
Use fall back
HTML:
<img fallback-src="http://google.com/favicon.ico" ng-src="{{image}}"/>
JS:
myApp.directive('fallbackSrc', function () {
var fallbackSrc = {
link: function postLink(scope, iElement, iAttrs) {
iElement.bind('error', function() {
angular.element(this).attr("src", iAttrs.fallbackSrc);
});
}
}
return fallbackSrc;
});
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.
I built a simple app with user authentication base on this: link
Basically, I have a userAccountService, responsible for communicating with server and login controller handling the login process.
From other controller I want to check if user is already logged in (to hide LogIn button, and show user profile instead).
So I have a navController
function navCtrl ($scope, $modal, userAccountService) {
$scope.IsUserLoggedIn = function () {
return userAccountService.isUserLoggedIn;
}
}
So in HTML I use this ng-hide="isUserLoggedIn()
my userAccountService:
app.factory('userAccountService', ['$http', '$q', userAccountService]);
function userAccountService($http, $q) {
var service = {
registerUser: registerUser,
loginUser: loginUser,
logOut: logOut,
getValues: getValues,
isUserLoggedIn: false,
accessToken: ""
};
// code ommited
function loginUser(userData) {
var tokenUrl = serverBaseUrl + "/Token";
if (!userData.grant_type) {
userData.grant_type = "password";
}
var deferred = $q.defer();
$http({
method: 'POST',
url: tokenUrl,
data: userData,
})
.success(function (data,status,headers,cfg) {
// save the access_token as this is required for each API call.
accessToken = data.access_token;
isUserLoggedIn = true;
// check the log screen to know currently back from the server when a user log in successfully.
console.log(data);
deferred.resolve(data);
})
.error(function (err, status) {
console.log(err);
deferred.reject(status);
});
return deferred.promise;
}
}
What am I doing wrong? Here's another interesting read I took inspiration from: link
You can't return a variable, but you can return a function, so create a function that returns that variable.
Try something like this, it returns your service object (you might want to put a $watch on it):
Service
function userAccountService($http, $q) {
function getData() {
return service;
}
...
}
Controller
$scope.IsUserLoggedIn = userAccountService.getData().isUserLoggedIn;
Also, you're not correctly updating the state variable from your success callback - you're creating global variables instead of using the service object properties. So, for example:
isUserLoggedIn = true;
should be:
service.isUserLoggedIn = true;