Hiding routes in Aurelia nav bar until authenticated
Asked Answered
B

4

14

Is there a proper way to hide items in the Aurelia getting started app behind some authentication.

Right now I'm just adding a class to each element based on a custom property. This feels extremely hacky.

    <li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}${!row.isVisible ? 'navbar-hidden' : ''}">
      <a href.bind="row.href">${row.title}</a>
    </li>
Bourgogne answered 6/3, 2015 at 1:26 Comment(0)
M
18

There are two directions you can take here.

The first is to only show nav links in the nav bar when the custom property is set like you are. To clean it up a bit let's use the show binding -

  <li repeat.for="row of router.navigation" show.bind="isVisible" class="${row.isActive ? 'active' : ''}">
    <a href.bind="row.href">${row.title}</a>
  </li>

The issue here is you still need to maintain the custom property like you are already doing. The alternative is to reset the router. This basically involves building out a set of routes that are available when the user is unauthenticated and then a separate set once the user is authenticated -

this.router.configure(unauthenticatedRoutes);
// user authenticates
this.router.reset();
this.router.configure(authenticatedRoutes);

This gives you the flexibility to reconfigure the router whenever you need to.

Mayes answered 6/3, 2015 at 14:26 Comment(0)
V
8

These answers are great, though for the purposes of authentication, I don't think any have the security properties you want. For example, if you have a route /#/topsecret, hiding it will keep it out of the navbar but will not prevent a user from typing it in the URL.

Though it's technically a bit off topic, I think a much better practice is to use multiple shells as detailed in this answer: How to render different view structures in Aurelia?

The basic idea is to send the user to a login application on app startup, and then send them to the main app on login.

main.js

export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .developmentLogging();

  // notice that we are setting root to 'login'
  aurelia.start().then(app => app.setRoot('login'));
}

app.js

import { inject, Aurelia } from 'aurelia-framework';

@inject(Aurelia)
export class Login {
  constructor(aurelia) {
    this.aurelia = aurelia;
  }
  goToApp() {
    this.aurelia.setRoot('app');
  }
}

I've also written up an in-depth blog with examples on how to do this: http://davismj.me/blog/aurelia-login-best-practices-pt-1/

Vito answered 22/9, 2015 at 16:54 Comment(6)
For securing a route, adding an "authorize" step, as mentioned in the docs worked well for me. Customizing the navigation pipelineRheumatism
Nice. These two methods do similar things. The logged in / logged out use case is better suited to multiple shells. The admin user / normal user use case is better for the authorize steps. In the former, a non logged in user has no access to the application, so you shouldn't load the application for them. Load a different "login" application. In the latter, the users are logged in and using the same application, but some users are restricted from particular routes.Vito
@Anthony, just want to note that the section you mentioned has moved in the docs. It's currently in Securing Your App but the url implies that it's specific to the current version and could change again in the futureExtradition
Even though I find your answer helpful and threw you an upvote, I'm not sure if it's not "overkill" (I know, there's no overkill when it comes to security, but still...). First, I feel @Anthony's solution is way simpler and is even doc'd. Second, with your approach, you would need to secure your app module, because a semi-skilled hacker could easily fiddle with the client code and ignore the login screen if you didn't do that. As the doc says, never trust the client, so in your case, the server should be responsible for securing the real app. (Yes, the auth step is prone to the same thing)...Burtonburty
... the bottom line is that I feel like this adds unnecessary complexity. If I was doing this in ASP.NET and returning the scripts/markup from secured controllers/actions, then: I wouldn't be able to do any bundling, since the markup would be generated by Razor and the scripts would need to be secured somehow as well. This of course implies that I'd even need to create my own logic where to find the views/modules since this extra layer of course would cause the paths to deviate from the normal...Burtonburty
... To sum it up, I feel there's nothing wrong with an unauthorized user being able to hack his way through the routing system, you shouldn't trust the client anyway. Secure your data instead on the server side, the worst thing that can happen like this is that a skilled hacker can view your markup, albeit with no data in it. However, you save yourself from implementing a lot of complicated stuff.Burtonburty
G
3

Although I like PW Kad's solution (it just seems cleaner), here's an approach that I took using a custom valueConvertor:

nav-bar.html

<ul class="nav navbar-nav">
    <li repeat.for="row of router.navigation | authFilter: isLoggedIn" class="${row.isActive ? 'active' : ''}" >

      <a data-toggle="collapse" data-target="#bs-example-navbar-collapse-1.in" href.bind="row.href">${row.title}</a>
    </li>
  </ul>

nav-bar.js

import { bindable, inject, computedFrom} from 'aurelia-framework';
import {UserInfo} from './models/userInfo';

@inject(UserInfo)
export class NavBar {
@bindable router = null;

constructor(userInfo){
    this.userInfo = userInfo;
}

get isLoggedIn(){
    //userInfo is an object that is updated on authentication
    return this.userInfo.isLoggedIn;
}

}

authFilter.js

export class AuthFilterValueConverter {
toView(routes, isLoggedIn){
    console.log(isLoggedIn);
    if(isLoggedIn)
        return routes;

    return routes.filter(r => !r.config.auth);
}
}

Note the following:

  • Your isLoggedIn getter will be polled incessantly
  • You can achieve the same with an if.bind="!row.config.auth || $parent.isLoggedIn" binding, but make sure that your if.bind binding comes after your repeat.for
Getraer answered 18/5, 2015 at 20:33 Comment(1)
This does not refresh the items in my navigation bar after a user logs in. What triggers refresh of navigation items when user's status changes from logged out to logged in and vice-versa?Nelson
A
3

I realize this is a bit of thread necromancy, but I wanted to add an answer because the accepted answer offers a solution that's explicitly recommended against by the Aurelia docs (you have to scroll down to the reset() method.

I tried several other methods, to varying degrees of success before I realized that I was looking at it wrong. Restriction of routes is a concern of the application, and so using the AuthorizeStep approach is definitely the way to go for blocking someone from going to a given route. Filtering out which routes a user sees on the navbar, though, is a viewmodel concern in my opinion. I didn't really feel like it was a value converter like @MickJuice did, though, as every example I saw of those were about formatting, not filtering, and also I felt like it's a bit cleaner / more intuitive to put it in the nav-bar view model. My approach was as follows:

// app.js
import AuthenticationService from './services/authentication';
import { inject } from 'aurelia-framework';
import { Redirect } from 'aurelia-router';

@inject(AuthenticationService)
export class App {
  constructor(auth) {
    this.auth = auth;
  }

  configureRouter(config, router) {
    config.title = 'RPSLS';
    const step = new AuthenticatedStep(this.auth);
    config.addAuthorizeStep(step);
    config.map([
      { route: ['', 'welcome'], name: 'welcome', moduleId: './welcome', nav: true, title: 'Welcome' },
      { route: 'teams', name: 'teams', moduleId: './my-teams', nav: true, title: 'Teams', settings: { auth: true } },
      { route: 'login', name: 'login', moduleId: './login', nav: false, title: 'Login' },
    ]);

    this.router = router;
  }
}

class AuthenticatedStep {
  constructor(auth) {
    this.auth = auth;
  }

  run(navigationInstruction, next) {
    if (navigationInstruction.getAllInstructions().some(i => i.config.settings.auth)) {
      if (!this.auth.currentUser) {
        return next.cancel(new Redirect('login'));
      }
    }

    return next();
  }
}

OK, so that by itself will restrict user access to routes if the user isn't logged in. I could easily extend that to something roles based, but I don't need to at this point. The nav-bar.html then is right out of the skeleton, but rather than binding the router directly in nav-bar.html I created nav-bar.js to use a full view-model, like so:

import { inject, bindable } from 'aurelia-framework';
import AuthenticationService from './services/authentication';

@inject(AuthenticationService)
export class NavBar {
  @bindable router = null;

  constructor(auth) {
    this.auth = auth;
  }

  get routes() {
    if (this.auth.currentUser) {
      return this.router.navigation;
    }
    return this.router.navigation.filter(r => !r.settings.auth);
  }
}

Rather than iterating over router.navigation at this point, nav-bar.html will iterate over the routes property I declared above:

<ul class="nav navbar-nav">
   <li repeat.for="row of routes" class="${row.isActive ? 'active' : ''}">
     <a data-toggle="collapse" data-target="#skeleton-navigation-navbar-collapse.in" href.bind="row.href">${row.title}</a>
   </li>
</ul>

Again, your mileage may vary, but I wanted to post this as I thought it was a fairly clean and painless solution to a common requirement.

Archespore answered 23/2, 2017 at 13:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.