I'm learning authentication using an Angular and Auth0 video on Udemy.
I've gotten to the point where I'm handling 401 errors and I'm receiving the following error:
angular.js:66 Uncaught Error: [$injector:cdep] Circular dependency found: auth <- redirect <- $http <- auth
And here is my angular configuration:
angular.config(config);
function config($provide, authProvider,
$urlRouterProvider, $stateProvider, $httpProvider, jwtInterceptorProvider) {
authProvider.init({
domain: 'cmckinstry.auth0.com',
clientId: 'Rmdm7tgPIWv1e1P6sKrBDoW8zI4kuOEa'
});
jwtInterceptorProvider.tokenGetter = function(store) {
return store.get('id_token');
}
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: 'components/home/home.tpl.html'
})
.state('profile', {
url: '/profile',
templateUrl: 'components/profile/profile.tpl.html',
controller: 'profileController as user'
});
function redirect($q, $injector, auth, store, $location) {
return {
responseError: function(rejection) {
if (rejection.status === 401) {
auth.signout();
store.remove('profile');
store.remove('id_token');
$location.path('/home');
}
return $q.reject(rejection);
}
}
}
$provide.factory('redirect', redirect);
$httpProvider.interceptors.push('redirect');
$httpProvider.interceptors.push('jwtInterceptor');
}
So, taking out the auth injection from the redirect function. But, then the redirect doesn't work properly. I'm suspecting that this has something to do with the authProvider, but I just can't seem to figure it out.
Look at the error message:
Circular dependency found: auth <- redirect <- $http <- auth
As stated in the error message, the circular dependency is formed because the authProvider uses the httpProvider which then uses the redirectProvider which completes a circle with the authProvider.
To break the circle, don't inject auth as a dependency in the redirectProvider construction function. Use the $injector service to inject the auth service when it is needed.
$provide.factory('redirect', redirect);
$httpProvider.interceptors.push('redirect');
//function redirect($q, $injector, auth, store, $location) {
function redirect($q, $injector, store, $location) {
return {
responseError: function(rejection) {
if (rejection.status === 401) {
//INJECT HERE
var auth = $injector.get("auth");
auth.signout();
store.remove('profile');
store.remove('id_token');
$location.path('/home');
}
return $q.reject(rejection);
}
}
}
Related
I'm experimenting with Angular, trying to setup a Angular frontend powered by a laravel backend (REST API). For authentication i want to use json web tokens. For Angular i'm using Satellizer (https://github.com/sahat/satellizer) and for Laravel JWT (https://github.com/tymondesigns/jwt-auth).
At the moment, i already can login with AngularJS using correct credentials stored in Laravel. The user info and token is stored in localStorage.
I would like to implement some kind of Angular service that can check if the user is authenticated in order to protect states form ui-router. I already tried a few tuts but i'm unable te get it working. Can someone point me in the right direction? Would be awesome!
loginController (Angular)
.controller('loginCtrl', [
'$scope',
'$rootScope',
'utils',
'$auth',
'$location',
'SweetAlert',
function ($scope,$rootScope,utils, $auth, $location, SweetAlert) {
$scope.login = function() {
var credentials = {
email: $scope.email,
password: $scope.password
};
$auth.login(credentials)
.then(function (response) {
var user = JSON.stringify(response.data.user);
localStorage.setItem('currentUser', user);
$location.path('/restricted.dashboard');
})
.catch(function (response) {
SweetAlert.swal("Inloggen mislukt.", "Controleer je email adres en wachtwood en probeer opnieuw.", "warning");
console.log("LOGIN NOT OK" + response);
});
};
app.states.js (ui-router, Angular)
.config([
'$stateProvider',
'$urlRouterProvider',
function ($stateProvider, $urlRouterProvider) {
// Use $urlRouterProvider to configure any redirects (when) and invalid urls (otherwise).
$urlRouterProvider
.when('/dashboard', '/')
.otherwise('/');
$stateProvider
.state("error.404", {
url: "/404",
templateUrl: 'app/componentsOld/pages/error_404View.html'
})
.state("error.500", {
url: "/500",
templateUrl: 'app/componentsOld/pages/error_500View.html'
})
// -- LOGIN PAGE --
.state("login", {
url: "/login",
templateUrl: 'app/components/auth/loginView.html',
controller: 'loginCtrl',
resolve: {
deps: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load([
'lazy_uikit',
'lazy_iCheck',
'app/components/auth/loginController.js',
'sweetAlert'
]);
}]
}
})
// -- RESTRICTED --
.state("restricted", {
abstract: true,
url: "",
views: {
'main_header': {
templateUrl: 'app/shared/header/headerView.html',
controller: 'main_headerCtrl'
},
'main_sidebar': {
templateUrl: 'app/shared/main_sidebar/main_sidebarView.html',
controller: 'main_sidebarCtrl'
},
'': {
templateUrl: 'app/views/restricted.html'
}
},
resolve: {
deps: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load([
'lazy_uikit',
'lazy_selectizeJS',
'lazy_switchery',
'lazy_prismJS',
'lazy_autosize',
'lazy_iCheck',
'lazy_themes',
'lazy_style_switcher',
'sweetAlert'
]);
}]
}
})
app.js (Angular)
....
// Satellizer configuration that specifies which API
// route the JWT should be retrieved from
$authProvider.loginUrl = 'zz/zz/laravel/public/api/authenticate';
// Redirect to the auth state if any other states
// are requested other than users
$urlRouterProvider.otherwise('/auth');
....
Laravel authenticateController (Laravel 5.2)
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
try {
// verify the credentials and create a token for the user
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong
return response()->json(['error' => 'could_not_create_token'], 500);
}
$user = Auth::user();
//$user->company;
//$street = $user->company->street;
// if no errors are encountered we can return a JWT
return response()->json([
"token" => $token,
"user" => $user
]);
}
What i would like to accomplish is creating a laravel Middleware for Angular so that we can check if a user is authenticated and in the future he or she has the right user role to load a Angular state.
Thanks for taking time for my issue. I'm looking forward to see how you would handle this :)
I found this solution somewhere on SO a while ago and it suits my needs(cant provide original answer yet, credits to the original answer). Event hough I use $routeProvider, I think you can apply similar logic with $stateProvider
.config(function ($routeProvider) {
$routeProvider
.when('/awesomeRoute', {
templateUrl: 'views/view.tpl.html',
controller: 'someAwesomeCtrl',
controllerAs: 'someCtrl',
resolve : {
//This function is injected with the AuthService where you'll put your authentication logic
'auth' : function(AuthService){
return AuthService.authenticate();
}
}
})
})
The Auth service:
.factory('AuthService', function($q, Service, $window, $location){
return {
authenticate : function(){
//Authentication logic here, some service to check
against backend if token you provided is valid and/or you are authorized to enter those sections of application,
or in your case with satellizer you can use `$auth.isAuthenticated()` method.
Service.isTokenValid(_yourToken).then( (res) => {
if(res.data === true){
return true;
} else {
return $q.reject('Not Authenticated');
}
}, (err) => {
$location.path('/');
return $q.reject('Not Authenticated');
})
} else {
return $q.reject('Not Authenticated');
}
}
};
})
and finally the "route error catcher", when there is something wrong, return home or to /auth route
.run(function($rootScope, $location){
//If the route change failed due to authentication error, redirect them out
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
if(rejection === 'Not Authenticated'){
$location.path('/');
}
});
})
I have created a new route 'Rooms' with the generator and have modelled my $stateProvider on the admin route
.state('admin', {
url: '/admin',
templateUrl: 'app/admin/admin.html',
controller: 'AdminController',
controllerAs: 'admin',
authenticate: 'admin'
});
Vs
$stateProvider
.state('rooms', {
url: '/rooms',
templateUrl: 'app/rooms/rooms.html',
controller: 'RoomsCtrl',
controllerAs: 'rooms',
authenticate: 'admin'
});
But my route still appears without authentication!
I guess I am missing a few things to make it secure, though I am unable to understand what!
Can anyone help?
Thanks
your controller should be like:
angular.module('auth8App').controller('RoomsCtrl', function ($scope,Auth,$location) {
//check if the user is logged-in
Auth.isLoggedInAsync(function(loggedIn) {
if (!loggedIn) {
//if the user is not logged Redirect to login
event.preventDefault();
$location.path('/login');
}
});
$scope.message = 'Hello';
});
check if you have an interceptor service factorie defined and called in your app.js lik this:
.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
$urlRouterProvider
.otherwise('/');
$locationProvider.html5Mode(true);
$httpProvider.interceptors.push('authInterceptor');
})
.factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) {
return {
// Add authorization token to headers
request: function (config) {
config.headers = config.headers || {};
if ($cookieStore.get('token')) {
config.headers.Authorization = 'Bearer ' + $cookieStore.get('token');
}
return config;
},
// Intercept 401s and redirect you to login
responseError: function(response) {
if(response.status === 401) {
$location.path('/login');
// remove any stale tokens
$cookieStore.remove('token');
return $q.reject(response);
}
else {
return $q.reject(response);
}
}
};
})
.run(function ($rootScope, $location, Auth) {
// Redirect to login if route requires auth and you're not logged in
$rootScope.$on('$stateChangeStart', function (event, next) {
Auth.isLoggedInAsync(function(loggedIn) {
if (next.authenticate && !loggedIn) {
event.preventDefault();
$location.path('/login');
}
});
});
});
Using angular-ui-router, I have something like:
.state('page', {
url: '/page',
templateUrl: '/page.html'
})
This template URL may return a "401 Unauthorized". Is it possible to handle the http response when the router tries to load the url and handle it, so I can show some message or redirect the user?
You can register an interceptor for your application. The implementation of this interceptor
$httpProvider.responseInterceptors.push([
'$q',
'$location',
'$rootScope',
(function($q, $location, $rootScope) {
return {
responseError: function(response) {
if (response.status === 401) {
console.log(response.status + ' intercepted');
$location.path('/unauthorized/');
$q.reject(response);
}
return response;
}
};
});
]
After this you need to register /unauthorized in your states with a custom page template.
I am trying to migrate my angularjs 1.2 app (which is working fine now) to angularjs 1.3. But I am getting the below error.
[$injector:modulerr] Failed to instantiate module constructionApp due
to: TypeError: Unable to get property 'push' of undefined or null
reference at Anonymous function (http://localhost/app/app.js:33:5)
Also, this is the code I am having in my app.js
var cmApp = angular.module('myApp',
['xeditable', 'ui.bindonce', 'gantt', 'angularFileUpload', 'ui.bootstrap', 'ui.router', 'ngAnimate', 'wc.Directives']);
cmApp.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
var interceptor = ['$rootScope', '$q', '$location', function (scope, $q, $location) {
function success(response) {
return response;
}
function error(response) {
var status = response.status;
if (status == 401 || status == 404) {
$location.path('#/Error');
return;
}
// otherwise
return $q.reject(response);
}
return function (promise) {
return promise.then(success, error);
}
}];
$httpProvider.responseInterceptors.push(interceptor); ***get error at this line
// For any unmatched url, redirect to /home
$urlRouterProvider.otherwise("/Home");
$stateProvider
.state('home', {
url: "/home",
templateUrl: "/app/views/home.html",
controller: "homeController"
})
.state('somepage', {
url: "/somepage/:ID",
templateUrl: "/app/views/somepage.html",
controller: "somePageController"
})
});
Please help me to solve this migration issue.
Im using Grails server side and Angular client side.
If the user is not allowed to access a page I redirect with Grails using response.sendError(404)
It is working but PrivateProfileController is calling before the redirection and it shows the template private.html for 1 seconds then redirecting to the template 404.html.
Is there a way to avoid displaying private.html before redirecting ?
Thank you
Grails controller's method
def privateProfile(){
if(NOT_AUTHORIZED){
return response.sendError(404)
}
}
Angular
app.config( ['$routeProvider', '$locationProvider',
'$tooltipProvider', '$compileProvider', '$httpProvider',
function($routeProvider, $locationProvider, $tooltipProvider,
$compileProvider, $httpProvider) {
$locationProvider.html5Mode(true) ;
$routeProvider
.when('/:username/private', {
templateUrl: '/js/angular/app/userprofile/templates/private.html',
controller: 'PrivateProfileController'
})
.when('/404', {
templateUrl: '/js/angular/app/error/404.html' ,
controller: 'HTTPErrorController'
})
.otherwise({
});
$httpProvider.responseInterceptors.push(
['$location', '$q', function ($location, $q) {
function success(response) {
return response;
}
function error(response) {
if (response.status === 404) {
$location.path('/404');
return $q.reject(response);
}
else {
return $q.reject(response);
}
}
return function (promise) {
return promise.then(success, error);
}
}]);
}
]).run(function ($rootScope) {
$rootScope.$on('$locationChangeStart',
function(evt, absNewUrl, absOldUrl) {
//console.log('Start : old', absOldUrl, 'new ',absNewUrl );
});
$rootScope.$on('$locationChangeSuccess',
function(evt, absNewUrl, absOldUrl) {
//console.log('Success : old', absOldUrl, 'new ', absNewUrl);
});
});;
app.controller("HTTPErrorController", function ($location) {
console.log("HTTPErrorController")
});
There are two components acting separately asynchronously:
rendering of the template based on the route
XHR request responding with the authorization status.
By the time the http response is intercepted (btw response interception is deprecated), the route completes its part by rendering content and bootstrapping the corresponding angular controller (PrivateProfileController). On receiving the response, the response gets intercepted and the routing is done to /404. There comes the latency of 1 sec. (The time it took to complete the call to Grails at server side plus interception)
What can be done is making the authorization call part of routeProvider's resolve for that particular route:
$routeProvider
.when('/:username/private', {
templateUrl: '/js/angular/app/userprofile/templates/private.html',
controller: 'PrivateProfileController',
resolve: {
authz: function($q){
//Create a promise
//Call the service (REST call to Grails)
//Get back 404
//Reject the promise if 404
//route to 404
//return promise
}
}
})
.when('/404', {
templateUrl: '/js/angular/app/error/404.html' ,
controller: 'HTTPErrorController'
})
.otherwise({
});
If routing to /404 is a problem inside resolve then, use $routeChangeError event which gets fired on rejected promise.
Refer this question answered by Misko himself and this post as well to see how resolve works.