Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Durandal login page redirect pattern

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?

like image 524
Chris Avatar asked Sep 06 '13 10:09

Chris


2 Answers

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 || "";
    }
},
like image 170
Chris Avatar answered Nov 08 '22 01:11

Chris


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.

like image 1
Yaser Moradi Avatar answered Nov 08 '22 02:11

Yaser Moradi