Angular - Loading a view without changing the url - javascript

I'm working on a project which has several views, which some of the views may not be accessible to a given user. What I want to do is that, if a user navigates to a url that is restricted (due to his permissions), lets say '/project/manhatten/security' I want to display a view (an html partial) which simply says 'Access Denied'.
But I want to display the view without changing the url. I want the url to stay '/project/manhatten/security', so the user can copy the url and give it to someone with enough permission, and it would work fine.
What is the best way to achieve this ? Can I still use ng-view or a combination of ng-view and ng-include ?
Thanks in Advance.

I don't know of a way on how to restrict access to a specific view in angular. I think you shouldn't restrict views. What you should do is restrict access to your api. So if a user doesn't have the privilege to delete a project. Simply send a 401 from the server when he calls the api. On the client side handle this 401 in angular with an $http interceptor.
I would do the following:
In your index.html create an element/directive to display the error message
index.html
<div ng-controller=ErrorMessageCtrl ng-show=error.message>
{{error.message}}
<ng-view></ng-view>
The ErrorMessageCtrl will get notified when an access denied error occured:
.controller('ErrorMessageCtrl', function ($scope) {
$scope.error = {}
$scope.$on('error:accessDenied', function(event, message) {
$scope.error.message = message
})
})
Create an interceptor service to handle http 401 auth error:
.factory('authErrorInterceptor', function ($q, $rootScope) {
return {
response: function (response) {
return response
},
responseError: function(rejection) {
if (rejection.status === 401) {
$rootScope.$broadcast('error:accessDenied', 'Access Denied')
}
return $q.reject(rejection)
}
}
})
add the interceptor service to $httpProvider
.config(function ($httpProvider, $routeProvider) {
$httpProvider.interceptors.push('authErrorInterceptor')
$routeProvider.when('/project/manhatten/security', {
template: '<div><h1>Secure Page</h1>secure data from server: {{data}}</div>',
controller: 'SecureDataCtrl'
})
})
$http.get('/api/some/secure/data') returns a 401 http status code
.controller('SecureDataCtrl', function ($scope, $http) {
$scope.data = 'none'
$http.get('/project/manhatten/security')
.success(function (data) {
$scope.data = 'secure data'
})
})
Please keep in mind that this was hacked in 10 minutes. You need to do some refactoring. Like creating an errorMessage directive which gets the error notification service injected and not broadcasting the error message to the scope.

Related

How to use different API's with different Authentication headers in the same Angular JS App

Hello I'm just a AngularJs beginner and want some advice/help from u guys!
I am making an app where you can login and check your current location in the building through the Cisco CMX server.
Its a server which calculates your position through the info of several access points you are connected with
I need to communicate with the mongodb for authenticating the users and I use a token for that.
And when I'm logged in I want to go to the API of the CMX with another authentication header.
But I cant see how it could work.
I set my default header in my app.js on run
$http.defaults.headers.common['Authorization'] = 'Bearer ' + $window.jwtToken;
And when I want to go to the CMX API i change my default header
$http.defaults.headers.common['Authorization'] = 'Basic AAAAAAAAAAAAAAAAAAAAA==';
But it wont do the trick..
Isn't it better to communicate with the CMX through the webserver itself instead of the client?
Thanks
In your case you want to intercept every HTTP request and inject it with an Authorization header containing a custom token.
to do that you need to use the angularjs Interceptors.
The interceptors are service factories that are registered with the
$httpProvider by adding them to the $httpProvider.interceptors array.
The factory is called and injected with dependencies (if specified)
and returns the interceptor
Here is an example of an interceptor that injects a token if it’s available in browser’s local storage.
$httpProvider.interceptors.push(['$q', '$location', '$localStorage', function ($q, $location, $localStorage) {
return {
'request': function (config) {
config.headers = config.headers || {};
if ($localStorage.token) {
config.headers.Authorization = 'Bearer ' + $localStorage.token;
}
return config;
},
'responseError': function (response) {
if (response.status === 401 || response.status === 403) {
$location.path('/signin');
}
return $q.reject(response);
}
};
}]);
code inspired from

Retrieving tokens from OAuth2 authorize endpoint in angularjs

I am trying to use IdentityServer with an Angularjs application. The Angularjs front end is meant to call the authorize endpoint when an unauthenticated user attempts to navigate to the root url of the application. The app.config has a resolve property which will generate a routeChangeError. This event triggers a redirect to the OAuth2 Authorization endpoint (IdentityServer in my case, running on Asp.Net MVC).
baseballApp.config(['$routeProvider', '$locationProvider', '$httpProvider', function($routeProvider, $locationProvider, $httpProvider) {
$routeProvider.when('/players', {
resolve: {
auth: function ($q, $rootScope) {
var deferred = $q.defer();
if ($rootScope.token) {
deferred.resolve();
} else {
deferred.reject();
}
return deferred.promise;
}
},
templateUrl: '/FrontEnd/Templates/PlayerList.html',
controller: 'PlayerListController'
});
$routeProvider.otherwise({ redirectTo: '/players' });
var spinnerFunction = function (data, headersGetter) {
if (data) {
$('#spinner').show();
return data;
}
};
$httpProvider.defaults.transformRequest.push(spinnerFunction);
$httpProvider.interceptors.push('authInterceptor');}]).run(['$rootScope', '$location', 'authenticationService', function ($rootScope, $location, authenticationService) {
//authenticationService.getBearerToken().then(function(data) {
// $rootScope.token = data;
//});
debugger;
var generateRandomString = function() {
return (Math.random().toString(16) + "000000000").substr(2, 8);
}
var nonce = generateRandomString();
var state = generateRandomString();
$rootScope.$on('$routeChangeError', function () {
window.location = 'https://localhost:44301/identity/connect/authorize?client_id=baseballStats&redirect_uri=https%3a%2f%2flocalhost%3a44300%2f&response_type=id_token+token&scope=openid+profile+roles+baseballStatsApi&nonce=' + nonce + '&state=' + state;
});}]);
As you can see I redirect to the authorize endpoint if I don't have a token. However, it's not clear to me how I can retrieve the tokens after I have signed in on the authorization endpoint.
I have specified a return url, and after sign in it does redirect to the application, but how would I set up my angular code to retrieve the token that is supposed to be generated on the Authorize endpoint and sent back to the application?
I have seen many examples of people using bearer token authentication for angular, but I have not seen anyone use the Authorize endpoint of an OAuth2 server with Angular. In MVC, there are many built in callbacks to retrieve the token. I am looking for some ideas on how to implement this in angular.
Could you please give me a hint as to what I'm doing wrong?
EDIT
I have also tried to pass a hash navigation tag in the redirect uri, but get an "Error unknown client" from IdentityServer. Please see the code below
$rootScope.$on('$routeChangeError', function () {
var returnUrl = encodeURIComponent('https://localhost:44300/#/players');
window.location = 'https://localhost:44301/identity/connect/authorize?client_id=baseballStats&redirect_uri=' + returnUrl + '&response_type=id_token+token&scope=openid+profile+roles+baseballStatsApi&nonce=' + nonce + '&state=' + state;
});
Anybody know why?
I presume you're using the implicit flow grant for your spa js app?
In that case, the identity server, after you've logged in, will send the access token back to a call back url of your spa app. Using that call back page you are able to capture to token in order to pass it along to your resource server within the authorization header as a bearer token.
Checkout this video from Brock Allen, very well explained:
https://vimeo.com/131636653
By the way, you should use the oidc-token-manager, written by Brock himself. It is very easy to use and it abstracts away all communication with the identity server. It's also available on bower so you can grab it from there.
Finally I would like to point you to my own spa angular app I've been working on lately: https://github.com/GeertHuls/SecuredApi.
It actually uses the token manager in order to obtain access tokens, auto-refresh them an expose them throughout the entire angular app.
Hope this will inspire you a little in order to integrate this with angular.
I sidestepped around this issue by creating a custom ActionResult which would return a JSON response with a token when the user was already logged in via .NET Auth cookies, it's a bit hacky and looks like this;
[AllowAnonymous]
public ActionResult GetToken()
{
if (!User.Identity.IsAuthenticated)
{
Response.StatusCode = 403;
return Json(new {message = "Forbidden - user not logged in."}, JsonRequestBehavior.AllowGet);
}
var claims = new ClaimsPrincipal(User).Claims.ToArray();
var identity = new ClaimsIdentity(claims, "Bearer");
AuthenticationManager.SignIn(identity);
var ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
var token = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
return Json(new { token }, JsonRequestBehavior.AllowGet);
}

$location.path(...) does not update page

I'm trying to move to a user's main page after successful login.
The browser's address bar is correctly updated by calling $location.path(), but the page is not reloaded.
I know it's a kind of a common problem, with several potential causes.
I think I am correctly inside angular lifecycle, since successful authentication is either triggered by an $http.post response or simulated via and angular $timeout (neither method works)
Adding $scope.apply() does not help anyway
Setting for example $location.absUrl('http://google.com') instead has no effect at all.
Here is the login controller:
var swControllers = angular.module('swControllers', []);
swControllers.controller('LoginCtrl', ['$scope', '$location', 'authentication',
function($scope, $location, authentication) {
authentication.ClearCredentials();
$scope.login = function () {
$scope.dataLoading = true;
authentication.Login($scope.email, $scope.password, function(response) {
$scope.dataLoading = false;
if (response.success) {
authentication.SetCredentials($scope.email, $scope.password);
$location.path('userHome');
//$location.absUrl('http://google.com');
//$scope = angular.element(document).scope();
//$scope.$apply();
} else {
$scope.error = response.message;
// $scope.dataLoading = false;
}
});
};
}]);
I have more general architectural concerns: is it cleaner to rely on ngRoute for this kind of stuff? Does this mean I'm limited to a single-page app?
Because I'm using Express + jade template views on the server side, and currently login page and userHome reside on two different templates.
I'm a bit dazzled by the interplay between server-side and client-side routing...

How to avoid $compile:tpload errors on 401 status code response

We are developing a Single Page Application with AngularJS and ASP.NET MVC Json Rest API.
When an unauthenticated client tries to navigate to a private route (Ex: /Foo/Home/Template) to get a template, it gets a 401 response from the Web API and our AngularJS app automatically redirects it to the login page.
We are handling the 401 with $http interceptor with something like this:
if (response.status === 401) {
$location.path(routeToLogin);
return $q.reject(response);
}
Entering the correct credentials allows the client to get the template.
Everything is working perfectly except for one detail; the Javascript console reports this error:
Error: [$compile:tpload] http://errors.angularjs.org/1.3.0/$compile/tpload?p0=%Foo%2FHome%2FTemplate%2F
AngularJs documentation states this:
Description
This error occurs when $compile attempts to fetch a template from some
URL, and the request fails.
In our AngularJs app the request fails but it is by design because the resource is there but it cannot be accessed (401).
Should I move on and accept this kind of error on console or is it possible to mute or shield it in some way?
EDIT:
I have debugged the angular source a little bit and I found what part of the code is raising the exception.
Since we are using TemplateUrl to declare our templates, we are indirectly using the function compileTemplateUrl that makes this call:
$templateRequest($sce.getTrustedResourceUrl(templateUrl))
this leaves the second parameter (ignoreRequestError) of templateRequest undefined.
ignoreRequestError(optional)boolean
Whether or not to ignore the exception when the request fails or the
template is empty
When our http interceptor, handling the 401 status code, rejects the promise, the $http.get inside the $TemplateRequestProvider fails and calls this function:
function handleError() {
self.totalPendingRequests--;
if (!ignoreRequestError) {
throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl);
}
return $q.reject();
}
I believe we can't do anything to prevent the error on console as TemplateUrl does not allow to set the ignoreRequestError flag to false.
I've tried to bypass the reject in case of 401 status code; this fixes the error on console but sadly it has a side effect: an empty template is wrongly cached into the TemplateCache causing othe problems.
After some thinking I remembered about decorating in Angular, it solved this problem perfectly:
app.config(['$provide', function($provide) {
$provide.decorator('$templateRequest', ['$delegate', function($delegate) {
var fn = $delegate;
$delegate = function(tpl) {
for (var key in fn) {
$delegate[key] = fn[key];
}
return fn.apply(this, [tpl, true]);
};
return $delegate;
}]);
}]);
You should be able to intercept the call for the template by status and url.
Plunker
app.config(function($httpProvider) {
var interceptor = function($location, $log, $q) {
function success(response) {
// The response if complete
$log.info(response);
return response;
}
function error(response) {
// The request if errors
$log.error(response);
return $q.reject(response);
}
return function(promise) {
return promise.then(success, error);
}
}
$httpProvider.responseInterceptors.push(interceptor);
});
As I see it, you have two options:
Option A)
go with the interceptors. However, to eliminate the compile you need to return success status code inside response error (BAD) OR redirect to the login page inside the interceptor (Good):
app.factory('authInterceptorService', function () {
var interceptor = {};
interceptor.responseError = function (rejection) {
if (rejection.status === 401 && rejection.config.url === "home template url") {
//BAD IDEA
//console.log("faking home template");
//rejection.status = 200;
//rejection.data = "<h1>should log in to the application first</h1>";
//GOOD IDEA
window.location = "/login.html";
}
return rejection;
}
return interceptor;
});
and on app config:
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptorService');
}
Option b)
make the home template public. After all it should be just html mark-up, without any sensible information.
this solution is clean...and perhaps is also possible.

Angularjs: Interceptor redirection creates double rendering

I am stuck with this strange behaviour. None of the Google hits seem to return any mention of a similar case so I am assuming that I am doing something seriously wrong.
My interceptor does react on the 401 status and redirects to the appropriate route but at the same time it renders the previous route too. So I have a very ugly flash of one template then the other. I am simply trying to test my authentication: if authenticated then render the table, otherwise render the login form. It does redirect to the login form but still flashes the table template for a fraction of a second.
angular.module('Basal', ['ngRoute', 'Basal.users'])
.config(function($routeProvider, $locationProvider, $httpProvider) {
$locationProvider.html5Mode(true);
$httpProvider.interceptors.push('authInterceptor');
$routeProvider.
when('/', {
templateUrl: '/tpl/table.html',
controller: 'MainCtrl'
}).
when('/login', {
templateUrl: '/tpl/login-form.html',
controller: 'MainCtrl'
}).
otherwise({
redirectTo: '/'
});
});
angular.module('Basal.users', [])
.controller('MainCtrl', function($scope, getJson) {
getJson.fetch(function (d){
$scope.computers = d;
});
})
.factory('getJson', function($http, $location) {
return {
fetch: function (c) {
$http.get("/json")
.success(function(data) {
console.log("getJson: Success!");
c(data);
})
.error(function() {
console.log("getJson: Failure!");
});
}
}
})
.factory('authInterceptor', function($q, $location) {
return {
'responseError': function(response) {
if (response.status === 401) {
$location.path('/login');
}
return $q.reject(response);
}
}
});
Now, when I hit '/' on the browser Angular does two requests on the background: one is to fetch the table template and insert it in to the view and the other is to get the JSON data.
On the server side I put session restriction only on the data request! The template is just a static file. To put restriction on the template I need to drag it through the server routing and this is not, I believe, how Angular does things. But if I do create server side route for the template and put session restriction on it then double rendering disappears.
I am confused about how this works. Fetching the template and fetching the data is done asynchronously in parallel. So while JSON request triggers 401 and redirects to the login template, the original table template is still going through and being rendered empty. Hence I get all the ugly double rendering. This is a race of some kind. How do I stop it? Maybe my interceptor is wrong? Isn't the Angular interceptor supposed to stop any further processing?
As a related issue, the otherwise function on the $routeProvider does not work either. If I put a non-existent URL, I get 404 from the server but Angular routing does not catch it. Is this how it is supposed to be? If a URL change happens in Angular and then I hit reload on the browser, I get an ugly 404 instead of a nice redirect. Am I supposed to handle 404 in an interceptor too? Then what is the point of otherwise?
Thanks.

Categories

Resources