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.
Implementation
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();
}
Limitations
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?