My project uses MVC to deliver the initial markup of my site
The MVC controller is super simple:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
I have my ng-app tag, bundling, and #RenderBody in a layout view:
<!DOCTYPE html>
<html ng-app="myAppName">
<head>
#Styles.Render("~/Content/css")
</head>
<body>
<div class="container body-content">
#RenderBody()
</div>
#Scripts.Render("~/bundles/aBundle")
</body>
</html>
And my Index view is stripped down as simple as possible:
<ng-view></ng-view>
My angular app.ts file looks like this:
module app {
var main = angular.module("myAppName", ["ngRoute", "breeze.angular"]);
main.config(routeConfig);
routeConfig.$inject = ["$routeProvider"];
function routeConfig($routeProvider: ng.route.IRouteProvider): void {
$routeProvider
.when("/home",
{
templateUrl: "app/views/homeView.html",
controller: "HomeController as vm"
})
.when("/itemDetail/:itemId",
{
templateUrl: "app/views/itemDetailView.html",
controller: "ItemDetailController as vm"
})
.when("/addItem",
{
templateUrl: "app/views/addItemView.html",
controller: "AddItemController as vm"
})
.when("/login",
{
templateUrl: "app/views/loginView.html",
controller: "LoginController as vm"
})
.otherwise("/home");
}
}
I can inspect the Request sent by the user in the MVC controller, or in the Razor view using #Request.IsAuthenticated to see if the user is logged in, but what is the best way to pass this information to my angular app so that I can properly route the user to a login page when they first sign on, but skip the login page if they have an active session on the server?
The research I have done to try and figure this out has suggested to me that I probably need to create an angular service to store a boolean value regarding whether the user is authenticated or not. Then, I need to add some code to check this service for every route using $routeChangeStart, and redirecting to the login page only when necessary. I have looked at many examples, but can't quite put the pieces together in the context of my own application.
Could someone help me connect the dots, please?
I'm developing a project that I'm not using razor. Only html and angular in the views. So... What I did is:
I created a directive "appWrapper" that holds the layout. Inside of it there is a section that has the ng-view.
This directive uses a controller, the AuthCtrl, and this controller uses a service, the AuthService.
Because of this, I have access to everything that's inside this controller in the entire html. So I could say ng-click="vm.logout()" in an item in the sidebar, for example.
This "AuthService" has some methods that can be called by the controller. Login and logout being 2 of them.
When I execute login, I set some cookies with some information I get back from my controller.
When I logout, I remove these cookies.
On the app.js, where my routes are, I have I .run at the end that checks these cookies every time the location (url) is going to be changed. If they exist, it lets the user continue. If not, it redirects the user to the login.
Did it help? If needed, I can post the code for you to use as example.
No need to create an action inside a controller for checking this. It would require your application to go to the server, back to the browser, back to the server and back to the browser for every action the user takes. This is not good.
I can share what I do in my own apps, and although I am using ui-router, the same technique can easily be applied to the built in router.
Basic logical workflow:
Automatically add a resolve to every route that fetches the user's current auth status and caches it.
Check if route requires authentication, and reject if user is not logged in, otherwise allow to proceed normally.
On $routeChangeError (or something similar) check to see what action to perform.
The advantage of this is it allows you to have a great deal of flexibility with regard to providing a nice client side experience with security.
Adding Credentials to Every Route:
let originalStateFunction = $stateProvider.state;
$stateProvider.state = function (state, config) {
//
// The "allowAnonymous" is something we added manually
// This will become important later because we might have
// routes that don't require authentication, like say
// the login page
//
if (angular.isDefined(config) && !config.allowAnonymous) {
_.defaults(config.resolve || (config.resolve = {}), {
/*#ngInject*/
userSession: function userSessionSecurityCheck($q, sessionService) {
let def = $q.defer();
sessionService.getSession()
.then(session => {
if(!session.isAuthenticated){
def.reject({
error:'AUTHENTICATION_REQUIRED'
});
}
});
return def.promise;
//You could also do more complex handling here...
// like check permissions for specific routes
// and reject the promise if they fail.
}
});
}
//Now call the original state/when method with our
// newly augmented config object
return originalStateFunction.apply(this, arguments);
};
Inspecting The Route Error
.run($rootScope => {
"ngInject";
$rootScope.$on('$stateChangeError', (event, toState, toParams, fromState, fromParams, error) => {
event.preventDefault();
if(error.error === "AUTHENTICATION_REQUIRED"){
//Now you can redirect the user appropriately
}
});
});
You can create a simple endpoint to return the current user's status by inspecting the Principal, and so long as you are smart about caching that on the client, you will only incur the hit once per user.
The full sample code is too large for SO, but hopefully this gives you a good starting point.
I am thinking you would do it along the lines of below:
MVC controller:
[Authorize] // Make sure we're authorising the whole controller.
public class ProfileController : Controller
{
public ActionResult IsLoggedIn()
{
return Json(this.Request.IsAuthenticated);
// you may need allow get as the second parameter
}
}
Angular:
app.factory('ProfileService', function ($http) {
var service = {
isLoggedIn: isLoggedIn
};
return service;
function isLoggedIn() {
return $http.get('Profile/IsLoggedIn').then(function (response) {
return response.data; // this depends on what the response object looks like
});
}
});
app.run(function (ProfileService, $location) {
ProfileService.isLoggedIn().then(function (isLoggedIn) {
if (!isLoggedIn) {
$location.path('/unauthorised'); // just redirect them to the unauthorised route.
}
});
});
This means that everytime your app runs it will check to see whether you are logged in. You can also use this profile service to go grab other information about the user! And you can pull this service into any other module you wish to perform this kind stuff.
Remember that it is pointless trying to secure the javascript because it is run in a sandbox. But always make sure that you use [Authorize] attributes in your MVC code so that the server is always enforcing Authorisation and Authentication.
Before anyone says this isn't Typescript, any Javascript is also valid Typescript. I leave it to the user to put in the type defs.
-- More Info --
You can add a xmin cache expiry on the is logged in or store these details into local storage if you are constantly requesting the information:
app.factory('ProfileService', function ($http, $q) {
var service = {
isLoggedIn: isLoggedIn
};
var cacheExpiries = {
loggedIn: { value: null, expiry: null }
};
return service;
function isLoggedIn() {
var cacheObj = cacheExpiries['loggedIn'];
var useCache = false;
if (cacheObj.expiry) {
useCache = new Date() < cacheObj.expiry;
}
if (useCache) {
// because http returns a promise we need to
// short circuit function with a promise
return $q(function (res, rej) {
res(cacheObj.value);
});
}
// set the new expiry for the cache, this just adds 5 minutes to now
cacheObj.expiry = new Date(new Date().setMinutes(new Date().getMinutes() + 5));
return $http.get('Profile/IsLoggedIn').then(function (response) {
cacheObj.value = response.data;
return response.data; // this depends on what the response object looks like
});
}
});
You could easily encapsulate the cache into a factory with a proper key value storage api. This would extract some of the logic out of your thin service ( to keep it thin )
Local Storage and Cookie Storage
I have used this module for years to do local storage access, this can be slotted in place of the cache or you could still have your cache service to wrap this storage solution so that you can completely stay decoupled from your dependencies.
Related
I want to redirect to another page without loading the currentpage.
let me explain my task. I am having user login condition.
If the user has not login and he tries to enter the URL directly EX("localhost/sample.html") means it will come to login page. For me this condition is working nicely.
First sample.html page open and then only it will redirect to login. The user able to see the Data in sample.html.
var logincheck = function () {
debugger;
$http.get('loggedin').success(function (user) {
alert(user);
// Authenticated
if (user != '0') {
refresh();
return;
}
// Not Authenticated
else {
$window.location.href = '/';
}
});
};
logincheck();
Is there any way to go login page without loading the sample.html page.
A interesting way do to this is by checking this condition in a $route event. For example, you can write this code in your app.run:
$rootScope.$on('$routeChangeStart', function(event, next, current) {
if(next.$$route && userNotAuthenticated) {
$location.href('/');
}
});
The advantage is that it will work for your whole app, you won't have to write code for each page.
In your main controller (the one attached to index) you should do a login check and set a variable to true/false. In your controller do a check on this variable, and do a $state.go() if user is not logged in.
Use ng-cloak directive in your index file where you attach your main controller like:
<body ng-controller="mainController" ng-cloak>
You can read up on ng-cloak here
This is how you can do it.
Whenever your angular app is boots, it runs the run function. So attach a location change listener in that function like below
App.run(['$rootScope', '$http', '$urlRouter', function($rootScope, $http, $urlRouter) {
$rootScope.$on('$locationChangeSuccess', function(event) {
event.preventDefault();// prevents the location change
$http({// make a http call to check if user is logged in or not.
url: '/path/to/check/auth',
method: 'post',
data: {}
}).then(function(response){
if(response.data.isUserLoggedIn){//If user is logged in.
$urlRouter.sync();
}else{//user is not logged in
window.location.assign('/login');
}
}, function() {
window.location.assign('/login');
});
});
$urlRouter.listen();
}]);
Suppose my web app needs to make a http request to get my site title, site description and so on. Since these variables are common to all pages, it makes sense to request those every time a user enter the site.
The question is, where do I make those calls? In run block? Or to create a root controller to do these tasks?
You can use one of these two approaches:
Make your call in run block and store the value in $rootScope and
use anywhere you want,
In your states, use resolve to get the page title and details , and
get it in the views , For ease use resolve in root state and use the
resolved variable as a dependency in other child or sibling routes
to get values..
$stateProvider.state('root', {
resolve:{
// Example using function with simple return value.
promiseObj: function($http){
// $http returns a promise for the url data
return $http({method: 'GET', url: '/someUrl'});
}
})
.state('sibling',{
controller:function($scope,promiseObj){
$scope.title = promiseObj.title;
}
})
You could define controller at the <html> level.
<html ng-app="app" ng-controller="initCtrl">
<head>
<title>{{ Page.title() }}</title>
...
You create service: Page and modify from controllers.
myModule.factory('Page', function() {
var title = //Code to get the title
.... //Other vars
return {
title: function() { return title; },
setTitle: function(newTitle) { title = newTitle }
...
};
});
Inject Page and Call 'Page.setTitle()' from controllers.
I am developping an OAuth Provider application, using AngularJS and ui-router.
For each state change, I do the following check:
If the user is already logged in:
1.1 If the user is not an admin -> redirect him to the callBackUrl
1.2 If the user is an admin, do nothing
If the user is not logged in:
2.1 If the user tries to access an admin page -> redirect him back to login
2.2 If not, do nothing
my ui-router run method is the following:
.run(function ($rootScope, $state, $auth, accountService, $window, $stateParams) {
$rootScope.$on('$stateChangeStart', function (e, toState, toParams, fromState, fromParams) {
accountService.getUser({ token: $auth.getToken() }).$promise
.then(function (response) {
if (response.isAdmin === false) {
e.preventDefault();
$window.location.href = $stateParams.callBackUrl;
return;
}
})
.catch(function (response) {
if (toState.name.split(".")[0] === 'admin') {
e.preventDefault();
$state.go('root.login');
}
});
});
});
Everything is OK except for the part where I redirectthe user to the callback URL using $window
$window.location.href = $stateParams.callBackUrl;
This redirection takes 2-3 seconds, and in the meantime my user can see the page he is trying to access on my application. I thought the use of preventDefault() would solve that but it doesn't. Do you know how I could hold the $statechangeevent so that the user is redirected directly to the callback URL?
Thanks
I would put it this way:
The above approach allows user everything, until he is not really proven to be UN-authenticated. Why that? Because the code is calling service and evaluating all the stuff once the data are received. Meanwhile - we trust the user.
So, I'd suggest to change the approach
NEVER trust the user. He/she must to do the best to prove he/she is the right one, to get somewhere ... (well, kind of that...)
I described one possible way (with working example) here:
Confusing $locationChangeSuccess and $stateChangeStart
Just a piece of code to cite
Th first part of the $rootScope.$on('$stateChangeStart', ...:
// if already authenticated...
var isAuthenticated = userService.isAuthenticated();
// any public action is allowed
var isPublicAction = angular.isObject(toState.data)
&& toState.data.isPublic === true;
// here - user has already proved that he is the one
// or the target is public (e.g. login page)
// let him go, get out of this check
if (isPublicAction || isAuthenticated) {
return;
}
The second part, user is not trusted, he requires access to private stuff
// now - stop everything
// NO navigation
// we have to be sure who user is, to continue
// stop state change
event.preventDefault();
// async load user
userService
.getAuthObject()
.then(function (user) {
var isAuthenticated = user.isAuthenticated === true;
if (isAuthenticated) {
// let's continue, use is allowed
$state.go(toState, toParams)
return;
}
// log on / sign in...
$state.go("login");
})
Check that, in action, here
I know this was asked a while ago, but here is a possible solution:
Ui-router actually provides a fantastic way to solve this. By using a "resolve" within your $stateProvider, you can check that the user is authenticated before the controller of that particular state is even instantiated. Here is what ui-router docs say about resolve:
Resolve
You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
https://github.com/angular-ui/ui-router/wiki - Section for resolve is almost half way down page
You can run accountService.getUser within the resolve to check for an authenticated user, and by doing so, would prevent someone who is not authed from seeing the view they are trying to route to.
The resolve is set up inside the $stateProvider, and may look something like this:
$stateProvider.state('myState', {
resolve: {
userAuth: function(accountService) {
return accountService.getUser();
}
}
)
If you notice in the above example, I set a property called userAuth within the resolve. This can now be injected into any controller or service and then you can check against it for authenticated users. Each state that needs to be a "protected" view can contain the resolve, and then the 2-3 second flash of the view won't occur, as the controller hasn't been instantiated, and the user is redirected to another state.
Let's say I have 4 routes - 2 require the user to be logged in, 2 do not. My app init looks like:
$routeProvider.when('/open1',{templateUrl:'/open1.html',controller:'Open1'});
$routeProvider.when('/open2',{templateUrl:'/open2.html',controller:'Open2'});
$routeProvider.when('/secure1',{templateUrl:'/secure1.html',controller:'Secure1'});
$routeProvider.when('/secure2',{templateUrl:'/secure2.html',controller:'Secure2'});
Routes /open1 and /open2 are open to all, while routes /secure1 and /secure2 require the user to be logged in and, if not, take some action, e.g. redirect to login or launch a warning. I can determine the user's state by using my Auth service and calling, e.g., Auth.isLogin(). So the result would be:
going to /open1 and /open2 always go to the template and controller declared above
if Auth.isLogin() returns true, /secure1 and /secure2 go to the above-declared template and controller
if Auth.isLogin() returns false, /secure1 and /secure2 take some other action, e.g. $location.path('/login')
I could put logic in the Secure1 and Secure2 controllers that checks, but that is repetitive and mixes up responsibilities, makes them harder to test, etc.
Is there a way that I can use the $routeProvider to declare, "check this route and this route and if not, redirect"? I was thinking of using resolve somehow, but not quite sure how to work it in (docs on resolve are not very clear, and few helpful examples).
EDIT:
based on the answers below, it appears there are three philosophies for doing this:
Using resolve to check logged in and fail the promise, and then catching the $routeChangeError event to redirect http://www.sitepoint.com/implementing-authentication-angular-applications/
Using just $routeChangeStart event to check logged in and redirect http://arthur.gonigberg.com/2013/06/29/angularjs-role-based-auth/
Using just resolve to check logged in and redirect http://midgetontoes.com/blog/2014/08/31/angularjs-check-user-login
The 2nd option is what the two answerers have suggested.
As in my comments above, there are 3 different paths (plus the ability to use a directive if you want to control it from within html templates). I ended up following
https://midgetontoes.com/angularjs-check-user-login/
which essentially is as follows:
$routeProvider.when('/secure', {
templateUrl: '/secure.html',
controller: 'Secure',
resolve:{
loggedIn:onlyLoggedIn
}
});
And then onlyLoggedIn:
var onlyLoggedIn = function ($location,$q,Auth) {
var deferred = $q.defer();
if (Auth.isLogin()) {
deferred.resolve();
} else {
deferred.reject();
$location.url('/login');
}
return deferred.promise;
};
Simple, works like a charm. If I ever need a directive, I will pull this piece into a service.
This blog post deals with user authentication in AngularJS using directives.
The $route service emits $routeChangeStart before a route change.
If you don't use directives, you can catch that event by calling app.run (you can place it after the code where you define the routes [app.config]). For example:
For full disclosure I use ui.router and this is an adapted code from $stateChangeStart I use in my app
var app = angular.module('app');
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/open1',{templateUrl:'/open1.html',controller:'Open1'});
$routeProvider.when('/open2',{templateUrl:'/open2.html',controller:'Open2'});
$routeProvider.when('/secure1',{templateUrl:'/secure1.html',controller:'Secure1'});
$routeProvider.when('/secure2',{templateUrl:'/secure2.html',controller:'Secure2'});
}]);
app.run(['$rootScope', '$location', 'Auth', function($rootScope, $location, Auth) {
$rootScope.$on('$routeChangeStart', function(event, currRoute, prevRoute){
var logged = Auth.isLogin();
//check if the user is going to the login page
// i use ui.route so not exactly sure about this one but you get the picture
var appTo = currRoute.path.indexOf('/secure') !== -1;
if(appTo && !logged) {
event.preventDefault();
$location.path('/login');
}
});
}]);
I had the same problem and I did it this way:
var app = angular.module('myModule',["ui-bootstrap"]);
And then listen for a locationchange in the app (this will also trigger onEnter of a page)
app.run(function ($rootScope, $location, $cookieStore) {
$rootScope.$on("$locationChangeStart", function (event, next, current) {
//Here you can check whatever you want (servercall, cookie...)
});
}
I Hope this helps!
I have angularjs project implemented multi-language and using ui-router for routing. Every language will be have different url. Ex:
http://example.com/!#/en-us/english-title
http://example.com/!#/es-es/spanish-title
All state with url registered automatically when app run and load them from database. Ex:
angular.module('bxApp').run(["$http", function ($http) {
$http.get('/Home/Routes').success(function (result) {
result = result || {};
if (angular.isDefined(result) && result !== null) {
_.each(result.Routes, function (route) {
stateProvider.state(route.Name, {
url: route.Url,
templateUrl: route.TemplateUrl,
controller: route.Controller,
});
});
}
});
}]);
It work well but it will not work when user copy this link and paste to browser or click this link from other website . I think because of state can't found so it will be redirect to default and it does not keep url that user enter or copy.
In this case , How to do that?
Thanks,
You're declaring your states as a result of an HTTP call to your server: the problem is that these states are defined too late for the user to navigate to them when he pastes the URL in a new tab.
To understand, let's deconstruct what happens :
The user is on the initial page / other website, and copies the URL.
He pastes it in a new tab
Your angular application loads, finishes its config phase without having declared any of those states, and sends an HTTP call.
ui-router fails to route to a state matching the pasted URL, since the corresponding state is not here yet, and redirects to default
The HTTP response comes back, and your states are created (but too late).
How to make it work ?
My first reaction would simply not to store your states on your server. Unless you want the very core of your UX to be language-dependent, you don't have to do that.
But hey, let's say we want to do it anyway. I suggest you try this : declare a toplevel 'language' state, and have it load the other states in a resolve clause. This will 'block' the routing until the other states are declared :
angular.module('bxApp')
.config(['$urlRouterProvider', function ($urlRouterProvider) {
$urlRouterProvider
.state('language',{
url: '/:language',
resolve: {
childrenLoaded: ['$http', function ($http) {
// returning a promise is essential to have the 'waiting' behavior
return $http.get('/Home/Routes').then(function (data) {
var result = data.result || {};
if (angular.isDefined(result) && result !== null) {
_.each(result.Routes, function (route) {
$stateProvider.state(route.Name, {
url: route.Url,
templateUrl: route.TemplateUrl,
controller: route.Controller
});
});
}
});
}]
}
})
}]);
Again, this approach is probably asking for trouble : I strongly recommend you hardcode your states instead of storing them in a database. If all that varies from one language to another is the text and URL, then you will be fine with an URL param.