16

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?

Chris
  • 44,602
  • 16
  • 137
  • 156
  • How about adding some extra functionality to method 1 which redirects a logged in user to a designated page (either the one he requested or his 'home' page)? That way you prevent the user from seeing the login screen by pressing the back button. – jp10k Sep 07 '13 at 09:56
  • I think this is a really interesting and general use case and may warrant some additions to the Durandal source. – Matthew James Davis Oct 15 '13 at 16:32

2 Answers2

6

Query-string pattern

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.

Implementation

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);
        }
        ...
    },
    ...
}

Limitation

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 || "";
    }
},
Chris
  • 44,602
  • 16
  • 137
  • 156
  • What does your "user' object look like? How is isAuthenticated implemented? – Dan Vanderboom Dec 24 '13 at 18:03
  • 2
    This worked for me, but make sure that if you're putting the fragment in the route, it is in the format `login*fragement` and not `login(/:fragement)`, otherwise you'll get view not found problems with paths like `#/login/user/5`. Also, `router.navigate(fragement, { replace: true, trigger: true });` will replace the history with the new location, so you can't go back to the login page. – Davis Jan 24 '14 at 17:23
  • Could just include `viewmodels/login` as a dependency (assuming is a singleton) and set the `.redirect` property with the fragment instead of passing it as argument – Alex Jan 12 '16 at 13:33
1

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.

Chris
  • 44,602
  • 16
  • 137
  • 156
Yaser Moradi
  • 3,267
  • 3
  • 24
  • 50
  • Thanks for your input. This is similar to the set up that I currently use (so it is good to know I am not doing something too unusual), except that I use `router.guardRoute` instead of a `canActivate` method on each view model. I use `navigator.goBack();` in my login view model (see final code block in my question) but this means that a user can navigate forward again to the login page once they have logged in, which is a behaviour that I am trying to avoid. How do you get around this? – Chris Sep 08 '13 at 10:45
  • I've no problem to see login page multiple times because I've several options in that view such as change password , change user level and ... It's good to use the canActivate of view model of login itself and then return false if current user is logged in already. – Yaser Moradi Sep 08 '13 at 20:08
  • Sure. I do have a `canActivate` method on the login in view itself, which prevents a user from navigating to the login page if they are already logged in. However, the limitations I outline in my question (see "Limitations of method 1") still stand. Do you see these behaviours? If not, can you add some code to your answer to demonstrate how your avoid them? – Chris Sep 12 '13 at 07:30