i want to implement an authentication/authorization system in angularjs, i found a couple of tutorials, and a lot of them using $routeChangeStart event and testing for a specific pages, and then ask a service to test if this user is authenticated by sending the token to the api.
and that's what i have done, but i didn't get the results that i need.
app.js
var app = angular.module("KhbyraApp", ['ngRoute', 'ngCookies']);
app.config(function ($routeProvider, $locationProvider, $httpProvider) {
$routeProvider
.when("/Register", {
controller: "RegisterController",
templateUrl: "/app/views/register.html",
authenticate : false
})
.when("/Login", {
controller: "LoginController",
templateUrl: "app/views/login.html",
authenticate: false,
})
.when("/Articles", {
controller: "ArticlesController",
templateUrl: "app/views/article.html",
authenticate: true
})
.otherwise({ redirectTo: '/Login' });
$locationProvider.html5Mode(true);
});
app.run(function ($rootScope, $location, $cookieStore, AuthService) {
$rootScope.$on("$routeChangeStart", function (event, next, current) {
if (next.authenticate) {
if (!AuthService.isAuthenticated()) {
$location.url("/Login");
};
};
});
AuthService.js
app.factory('AuthService', function ($http, $q, $window, $cookieStore) {
var factory = {};
var loginUrl = 'http://localhost:2399/Token';
var authUrl = 'http://localhost:2399/Authenticate';
var email;
var token;
factory.Authenticate = function (email, password) {
console.log("AuthService -" + email);
var deferred = $q.defer();
var user = {
email: window.btoa(email),
password: window.btoa(password)
};
$http({
method: 'POST',
url: loginUrl,
data: user
}).success(function (data) {
console.log("AuthService - " + data);
token = data.replace('"', '').replace('"', '');
email = user.email;
deferred.resolve(token);
$cookieStore.put('token', token);
$cookieStore.put('email',user.email);
}).error(function () {
deferred.reject();
});
return deferred.promise;
};
factory.Email = email;
factory.Token = token;
factory.isAuthenticated = function () {
var request = $http({
method: 'GET',
url: authUrl,
headers: { 'Authorization': 'Token '+ $cookieStore.get('email') + ":"+ $cookieStore.get('token') }
}).then(function () {
return true;
}
,function () {
return false;
});
};
return factory;
});
the problem here is in the routeChangeStart even if the AuthService.isAuthenticated() returns true, in the if statements something goes wrong, i think it's about $http returns a promise.
It's normal that the route is loaded even if you're condition fails : Angular doesn't rollback and go back to the previous page. The RouteChangeStart event is actually called just after the URL is changed, when angular detects it.. but the redirection has already been made (there is actually no BeforeRouteChange event)
So you'll have to handle it yourself depending on your needs. For example, in this case, you'll typically force a redirection to the login page. YOu could also display a login popup on top of your page and wait for the login to be successfull to re-execute the previously failed request (which should now works because you're are now logged again). This behavior is exaclty the one of the http-auth-interceptor.
See also angular-app which implements something similar (based on http-auth-interceptor also)
Related
I am using Laravel angularjs
I am using this package https://github.com/andbet39/tokenAuth
it's working fine but my problem is without login i can go to any page also once i reload the page user name is disabled
I don't know what is the problem here
app.js
var app = angular.module('todoApp', ['ui.router', 'satellizer'])
.config(function($stateProvider, $urlRouterProvider, $authProvider,$provide) {
$authProvider.loginUrl = '/api/authenticate';
$urlRouterProvider.otherwise('/login');
$stateProvider
.state('login', {
url: '/login',
templateUrl: '/js/tpl/login.html',
controller: 'AuthController'
})
.state('register', {
url: '/register',
templateUrl: '/js/tpl/register.html',
controller: 'AuthController'
})
.state('todo', {
url: '/todo',
templateUrl: '/js/tpl/todo.html',
controller: 'TodoController'
});
function redirectWhenLoggedOut($q, $injector) {
return {
responseError: function (rejection) {
var $state = $injector.get('$state');
var rejectionReasons = ['token_not_provided', 'token_expired', 'token_absent', 'token_invalid'];
angular.forEach(rejectionReasons, function (value, key) {
if (rejection.data.error === value) {
localStorage.removeItem('user');
$state.go('login');
}
});
return $q.reject(rejection);
}
}
}
$provide.factory('redirectWhenLoggedOut', redirectWhenLoggedOut);
});
TodoController.js
app.controller('TodoController', function($state,$http,$rootScope, $scope,$auth) {
$scope.todos=[];
$scope.newTodo={};
$scope.init = function (){
$http.get('/api/todo').success(function(data){
$scope.todos=data;
})
};
$scope.save = function(){
$http.post('/api/todo',$scope.newTodo).success(function (data) {
$scope.todos.push(data);
$scope.newTodo={};
});
};
$scope.update = function(index){
$http.put('/api/todo/'+ $scope.todos[index].id,$scope.todos[index]);
};
$scope.delete = function(index){
$http.delete('/api/todo/'+ $scope.todos[index].id).success(function(){
$scope.todos.splice(index,1);
});
};
$scope.logout = function() {
$auth.logout().then(function() {
localStorage.removeItem('user');
$rootScope.authenticated = false;
$rootScope.currentUser = null;
});
}
$scope.init();
});
AuthController.js
app.controller('AuthController', function($auth, $state,$http,$rootScope, $scope) {
$scope.email='';
$scope.password='';
$scope.newUser={};
$scope.loginError=false;
$scope.loginErrorText='';
$scope.login = function() {
var credentials = {
email: $scope.email,
password: $scope.password
}
$auth.login(credentials).then(function() {
return $http.get('api/authenticate/user');
}, function(error) {
$scope.loginError = true;
$scope.loginErrorText = error.data.error;
}).then(function(response) {
// var user = JSON.stringify(response.data.user);
// localStorage.setItem('user', user);
$rootScope.authenticated = true;
$rootScope.currentUser = response.data.user;
$scope.loginError = false;
$scope.loginErrorText = '';
$state.go('todo');
});
}
$scope.register = function () {
$http.post('/api/register',$scope.newUser)
.success(function(data){
$scope.email=$scope.newUser.email;
$scope.password=$scope.newUser.password;
$scope.login();
})
};
});
I want to redirect to login page if authandicate is falied
How to fix this ?
In angularjs 1.4+ there is no
$http.get('/api/todo').success(function(data){
$scope.todos=data;
})
What you should do instead
$http.get('/api/todo').then(function(data){
$scope.todos=data;
})
And same with this $http.post which you have below.
Also after refreshing page rootScope is deleted and that is why nickname is blank after refresh.
You probably want to store nickname in localStorage or async promise based localForage.
If you chose async localForage on login you can emit custom event with rootScope and execute some function on this event which gather nickname from localForage. You might want to execute this function in some external controller which would wrap all app so when you assign $scope.nick you will have access to it across entire app. Same with $scope.auth = true, you will be able to build your app basing on this boolean for logged in using ng-if directive.
Inject $location to your controller as function parameter and try to redirect like so
$location.path('/todo' );
or
$location.url(YOUR_URL);
Also I don't really understand why you are doing two backend call for login, one inside another. You probably should do one $http.post which would return token in response. Then you could fix and simplify your function code to
$scope.login = function() {
var credentials = {
email: $scope.email,
password: $scope.password
}
$auth.login(credentials).then(function(response) {
$rootScope.authenticated = true;
$rootScope.currentUser = response.data.user;
$scope.loginError = false;
$scope.loginErrorText = '';
}, function(error) {
$scope.loginError = true;
$scope.loginErrorText = error.data.error;
$location.path('/todo' );
});
}
However I don't know your code from $auth service.
Remember to inject $location service.
redirectWhenLoggedOut seems to be an http interceptor.
I think the idea is that you redirect when the http call was not successful. So you need to add an http interceptor that catches the http error and redirects to the login page.
$httpProvider.interceptors.push('redirectWhenLoggedOut');
Don't forget to inject the $httpProvider;
EDIT: forgot to mention that i've been working with AngularJs for a week only, so if you see something you think should be changed for the better and is not related to the question itself feel free to tell me on the comments section.
ok, so I have my authentication Controllers and providers which I won't show because they're irrelevant for the scope of the question.
Then I have an interceptor to check if the user is authenticated when a Call is made. If so I set the Authentication header on the request to include the user's Token if not I redirect the user to the login page and don't even make the request to the server (obviously if someone bypasses this theres also an Authorize on the API).
What I want is to add a few exceptions, meaning there are some pages I want to allow even if the user has no Auth Token. I'm able to this if it's a specific path, but I want to allow my 404 page to be accessed and it's in the Routing that I'm specifying .otherwise to go to the 404 page, how can I make so that my interceptor only redirects to login if it's not going to this page.
The interceptor
.factory('authInterceptorService', ['$q', '$location', 'localStorageService', function ($q, $location, localStorageService) {
var authInterceptorServiceFactory = {};
var authData = localStorageService.get('authorizationData');
var _request = function (config) {
config.headers = config.headers || {};
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
} else if ($location.path != '/accounts/login' && $location.path != '/accounts/register') {
$location.path('/accounts/login');
}
return config;
}
var _responseError = function (rejection) {
if (rejection.status === 401) {
$location.path('/accounts/login');
}
return $q.reject(rejection);
}
authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
return authInterceptorServiceFactory;
}])
and in my Routing
$urlRouterProvider.otherwise('/page-not-found');
$stateProvider
(...)//rest of the states
.state('page-not-found', {
url: '/page-not-found',
templateUrl: '/Content/partials/error/404.html',
data: {
displayName: false
}
})
(...)//rest of the states
I tried to add '/page-not-found' to my if but it won't work as expected because by the time the location is checked for the first time it's still not redirected.
edit
As sugested by charlietfl I'm now trying to use resolve but it's not even passing my function.
I removed this code from my interceptor:
else if ($location.path != '/accounts/login' && $location.path != '/accounts/register') {
$location.path('/accounts/login');
}
and add a new service to the authentication module:
.service('authCheckService', ['$http', '$q', 'localStorageService', function ($http, $q, localStorageService) {
var self = {
'onlyLoggedIn': function ($state, $q) {
var deferred = $q.defer();
var authData = localStorageService.get('authorizationData');
console.log(authData);
if (authData) {
deferred.resolve();
} else {
deferred.reject();
$state.go('login');
}
return deferred.promise;
}
}
return self;
}]);
and i'm trying to call it as:
.state('smo-dashboard', {
url: '/dashboard',
templateUrl: '/Content/partials/dashboard.html',
resolve: authCheckServiceProvider.onlyLoggedIn
})
notice that i'm trying to log authData var to check if it's working but it isn't and there's no error on the console also.
Finally figured out how to solve it using resolve.
first of all I completely removed the interceptor I was using before.
then I made a function inside my Routing .config to use with every resolve for the authentication. finally to handle my resolve I'm using $stateChangeError to redirect to the login state
the Routing Config
.config(function ($stateProvider, $urlRouterProvider) {
// function to check the authentication //
var Auth = ["$q", "authService", function ($q, authService) {
authService.fillAuthData;
if (authService.authentication.isAuth) {
return $q.when(authService.authentication);
} else {
return $q.reject({ authenticated: false });
}
}];
/* if the state does not exist */
$urlRouterProvider
.otherwise('/page-not-found');
$stateProvider
// state that allows non authenticated users //
.state('home', {
url: '/',
templateUrl: '/Content/partials/home.html',
})
// state that needs authentication //
.state('smo-dashboard', {
url: '/dashboard',
templateUrl: '/Content/partials/dashboard.html',
resolve: {
auth: Auth
}
})
// errors //
.state('page-not-found', {
url: '/page-not-found',
templateUrl: '/Content/partials/error/404.html'
})
// accounts //
.state('login', {
url: '/accounts/login',
templateUrl: '/Content/partials/account/login.html'
})
// OTHER STATES //
}
);
in the MainController
$scope.$on("$stateChangeError", function (event, toState, toParams, fromState, fromParams, error) {
$state.go("login");
});
An error service like this could help to handle what to do according to status in responses:
'use strict';
/**
* Error Service
*/
angular.module('app.errorService', [])
.factory("errorService", function ($route, $location) {
return {
checkAndReturnError: function(a,b,c) {
if (a.status === 401){
(function(){
return $location.path("/accounts/login");
}());
return;
}
if (a.status === 404)
return;
alert("Error \n *" + a.data.message);
}
};
});
Then when you do your calls if the response status is 401 it will redirect. The vbad thing agout this is you have to add it to all calls:
$scope.pageChanged = function() {
$scope.Promise = Resource.get({}, function(response) {
}, errorService.checkAndReturnError);
};
$http.post('http://localhost:7001/v1/sessions', {
data: {
username: $scope.user.username,
password: $scope.user.password,
type: 'sessions'
}
})
.then(function(response) {
if(response.data.data.token) {
$http.defaults.headers.common.Authorization = response.data.data.token;
$state.go('app.dashboard');
} else {
$scope.authError = response;
}
}, function(x) {
$scope.authError = 'Server Error';
});
I can confirm that the if condition gets called and a response.data.data.token is present.
It goes to the app.dashboard state but is intercepted by my ui-router:
$stateProvider.state('app', {
abstract: true,
url: '/app',
templateUrl: 'tpl/app.html',
resolve: {
current_user: ['$http', function($http) {
return $http.get('http://localhost:7001/v1/users/4/entities');
}]
}
})
That call, however, does not have anything set in the header. I thought that $http.defaults would set a default value in the header. What am I doing incorrectly?
You must set the default headers in the config method and not in your service.
Example:
myApp.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.common['Content-Type'] = 'application/json; charset=utf-8';
}]);
Only in config you can configure the httpProvider. If you try to do that inside your service, it won't affect the $httpProvider service at all.
EDIT:
You must make use Interceptors in this scenario.
For purposes of global error handling, authentication, or any kind of
synchronous or asynchronous pre-processing of request or
postprocessing of responses, it is desirable to be able to intercept
requests before they are handed to the server and responses before
they are handed over to the application code that initiated these
requests.
Refer Angular Docs Interceptor section
Just some sample code:
app.service('APIInterceptor', function($rootScope, UserService) {
var service = this;
service.request = function(config) {
// check if the token is available. Once the token is available get it here from the UserService.
var access_token = UserService.getToken() || "unauthorized";
if (access_token) {
config.headers.authorization = access_token;
}
return config;
};
service.responseError = function(response) {
return response;
};
})
In your config
$httpProvider.interceptors.push('APIInterceptor');
I would prefered you to one service to use sharable data.
Code
app.service(dataService, function(){
this.data = {}
this.getData = function(){
return data;
};
this.setTokenData = function(token){
data.token = token;
}
});
Now your code would be while setting token you could use dataService
if(response.data.data.token) {
dataService.setTokenData(response.data.data.token);
$http.defaults.headers.common.Authorization = dataService.data.token; //dataService.getData().token;
$state.go('app.dashboard');
} else {
$scope.authError = response;
}
Then from service resolve you could use
$stateProvider.state('app', {
abstract: true,
url: '/app',
templateUrl: 'tpl/app.html',
resolve: {
current_user: ['$http', 'dataService', function($http, dataService) {
$http.defaults.headers.common.Authorization = dataService.getData().token;
return $http.get('http://localhost:7001/v1/users/4/entities');
}]
}
})
I have a SPA using JWT (json web tokens) for authorization to the api. The problem is when they hit refresh on the browser after being logged in I need to verify the token is still valid via an ajax request and then continue loading the SPA. I added this to the .run() which kind of works, but since my navigation changes if they are logged in, the page loads before the token is verified, and looks wrong. I'm new to angular, but guess this could be done with a promise?
// handle refreshing browser
if ($window.sessionStorage.token && !AuthenticationService.isLogged()) {
UserService.verify().success(function (data) {
AuthenticationService.setLogged(true);
}).error(function () {
delete $window.sessionStorage.token;
$state.transitionTo('app.login');
});
}
I was able to get this working with the following state setup:
$stateProvider
.state('app', {
abstract: true,
url: '/',
views: {
root: {
templateUrl: 'tpl/index.html'
}
},
resolve: {
User: ['$window', '$q', 'AuthenticationService', 'UserService' , function($window, $q, AuthenticationService, UserService) {
if ($window.sessionStorage.token && !AuthenticationService.isLogged()) {
var d = $q.defer();
UserService.verify().success(function (data) {
AuthenticationService.setLogged(true);
d.resolve();
}).error(function () {
delete $window.sessionStorage.token;
d.resolve();
});
return d.promise;
}
}]
}
});
In my AngularJS application on every request to change the page i run :
$rootScope.$on('$locationChangeStart', function (event, next, current) {
var user;
$http.get('/api/Authentication/UserAuthenticated').then(function (data) {
console.log("call");
user = data.data;
});
console.log("end of call");
});
When i run application and test what is happening i see in console that "end of call" is returned before console.log("call"); which means that user is not set. Which means that if i want to check if user is logged in on change of route user will be undefined.
How do i make Angular run-> http request and only then keep going?
I misunderstood the question a bit. You can let the $routeProvider resolve the $http promise:
var app = angular.module("myApp");
app.config(["$routeProvider", function($routeProvider) {
$routeProvider.when("/",{
templateUrl: "myTemplate.html",
controller: "MyCtrl",
resolve: {
user: ["$http", "$q", function($http, $q) {
var deferred = $q.defer();
$http.get('/api/Authentication/UserAuthenticated').success(function(data){
deferred.resolve(data.data);
}).error(function(error) {
deferred.resolve(false);
});
return deferred.promise;
}]
}
});
}]);
If the code to fetch the user data is too complex, you could create a service for it, and inject that service in the $routeProvider's resolve function.
In your controller, you just inject the promise (which will be resolved):
app.controller("MyCtrl",["$scope","user", function($scope, user) {
if (!user) {
alert("User not found");
}
...
}]);
use async:false. It is working for me
Try this code, instead of your code
$rootScope.$on('$locationChangeStart', function (event, next, current) {
$http({method: 'GET',
url: '/api/Authentication/UserAuthenticated',
async: false
}).success(function (data) {
console.log("call");
user = data.data;
}
console.log("end of call");
});