TL;DR What is a good pattern for requiring a user to login in order to view certain pages in a Durandal Single Page Application (SPA)?
I need a system whereby if a user attempts to navigate to a "page" that requires them to be logged in, they are instead redirected to a login page. Upon successfully authenticating on this login page, I would then like the application to redirect them to the page that they previously attempted to access before they were redirected to the login page.
One method that I can think of which may be implemented for redirecting a user to and from a login page is to store the URL to the page which requires authentication in the browser history and, after successfully authenticating, navigate back to this history item from the login page.
I have attempted to implement the pattern described above but have a few issues with it. In main.js
(or anywhere before router.activate()
is called) I guard the route which requires authentication:
router.guardRoute = function (instance, instruction) {
if (user.isAuthenticated()) {
return true;
} else {
if (instance && typeof (instance.allowAnonymous) === "boolean") {
if (!instance.allowAnonymous) {
/* Use one of the following two methods to store an item in the
browser history. */
/* Add guarded URL to the history and redirect to login page.
* We will navigate back to this URL after a successful login.
*/
if (history.pushState) {
history.pushState(null, null, '#' + instruction.fragment);
}
else {
location.hash = '#' + instruction.fragment;
}
*/
/* This will not work - history is not updated if the fragment
* matches the current fragment.*/
////router.navigate(instruction.fragment, false);
/* The following solution puts in a fragment to the history
* that we do not want the user to see. Is this an
* acceptable solution? */
router.navigate(instruction.fragment + 'LoginRedirect', false);
return router.convertRouteToHash("login");
}
}
return true;
}
};
In shell.js
I add a route to the login page:
return {
router: router,
activate: function () {
router.map([
...
{ route: 'login', title: '', moduleId: 'viewmodels/login', nav: true },
...
]).buildNavigationModel();
return router.activate();
},
...
}
In viewmodels/login.js
I then have the code snippet:
if (account.isAuthenticated()) {
router.navigateBack();
}
One limitation of this method is that it allows the user to navigate forward to the login page after they have authenticated and been redirected away from the login page.
Furthermore, using
router.navigate(instruction.fragment + 'LoginRedirect', false);
I occasionally see LoginRedirect
flash up in the URL.
A more serious limitation is that a user pressing back on the login page will not be taken to the previous (non-guarded) page (but instead will be taken to the guarded page, which will redirect to the login page).
It also seems that there is a bug with the guardRoute
letting page refreshed through (see here. Is this still an issue?
Are they any standard Durandal patterns which encapsulate the above behaviour and don't have similar limitations as those listed above?
Attempting to implement the "navigate back" method described in my question I have decided against using this method because of the limitations I have described above.
I have found in practice that a method which works much better is to pass the URL to the page which requires authentication as a query string to the login page, which can then use this to navigate forward to this URL once a user is authenticated.
Below is an outline of an implementation of this method which I have adopted. I am still keen to learn about any other login patterns that people have adopted for use in Durandal applications however.
In main.js
(or anywhere before router.activate()
is called) I still guard the route which requires authentication:
router.guardRoute = function (instance, instruction) {
if (user.isAuthenticated()) {
return true;
} else {
if (instance && typeof (instance.preventAnonymous) === "boolean") {
if (instance.preventAnonymous) {
return 'login/' + instruction.fragment;
}
}
return true;
}
};
In shell.js
:
return {
router: router,
activate: function () {
router.map([
...
{ route: 'login', title: '', moduleId: 'viewmodels/login', nav: true },
{ route: 'login/:redirect', title: '', moduleId: 'viewmodels/login', nav: true },
...
]).buildNavigationModel();
return router.activate();
},
...
}
In viewmodels/login.js
:
viewmodel = {
...,
canActivate: function() {
if (!user.isAuthenticated()) {
return true;
}
return false;
},
activate: function(redirect) {
viewmodel.redirect = redirect || "";
},
loginUser() {
...
if (user.isAuthenticated()) {
router.navigate(viewmodel.redirect);
}
...
},
...
}
One minor negative of this method is the prescense of the page fragment to redirect to upon a succesful login in the application URL query string. If you do not like the prescence of this page fragment in the URL query string, local storage could easily be used instead to store this value. In router.guardRoute
one could simple replace the line
return 'login/' + instruction.fragment;
with
/* Check for local storage, cf. http://diveintohtml5.info/storage.html */
if ('localStorage' in window && window['localStorage'] !== null){
localStorage.setItem("redirect", instruction.fragment);
return 'login/';
} else {
return 'login/' + instruction.fragment;
}
and our activate
method could look like:
activate: function(redirect) {
if ('localStorage' in window && window['localStorage'] !== null){
viewmodel.redirect = localStorage.getItem("redirect");
} else {
viewmodel.redirect = redirect || "";
}
},
My method:
I've developed a view model base function, which is base for all view models of my application. In the canActivate
method (in view model base), I'll return false
and navigate to login page if the current user does not have permission to view that page. I also perform a query against a client side database (to determinate user has access to page or not).
The good news is that Durandal supports deferred execution for canActivate
. In the login page, after a successful login, I'll use navigator.goBack();
to return to previous page.
I hope this helps you.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With