Meteor: Using Flow Router for authentication and permissions
Update May 26th 2016:
Added fix for Roles subscription not being ready. See part 3 for more details.
I recently switched from Iron Router to Flow Router and it’s the best decision ever. Iron Router (IR) has serious problems. I’d even go so far as to say the architecture is flawed, and the average of 270 open Git issues serve as proof that I’m not wrong.
IR has serious issue with reactivity in terms of routes re-running unpredictably, hooks not doing what they say they’re doing (like onBefore doesn’t really run before the route runs, resulting in screen flickering before the hook kicks in) and unreliable subscription management.
Actually, only with a lot of hacking around I’ve been able to secure my meteor app using IR. So I’ve decided to start using Flow Router (FR) instead. FR feels super solid, stable and predictable. It simplifies routing greatly so I definitely recommend using it.
In this guide I will show you how you can secure your app using Flow Router.
The scenario
I’ll be implementing the following scenario in this guide:
- There are routes that are public
- There are routes that are only accessible for logged in users
- There are routes that are only accessible for logged in users that are admins
- When logging out, the user should be redirected from the page he’s currently visiting
- When the user is logged in in 1 client, and logs in in another client, the other client should log the user out and redirect him away from the current page.
One requirement is that routes should really not be accessible, which means they should not be rendered and no Template helpers / events / subscriptions / logic should run if the user doesn’t have the rights.
So this guide is an alternative to the template-based authentication as proposed by the official Flow Router guide. I highly disagree with this pattern, hence this guide :-)
Using Flow Group Routes
Flow Group Routes enable you to group your routes, which means they share functionality defined in the group. You can specify hooks, and prefixes, but for a complete overview check the documentation. Group routes are really the way to go for authentication. I think it’s the most awesome feature about Flow Router. So lets get started.
1. Routes that are Public
Lets make a group for routes that are public, which means accessibly to anyone.
exposed = FlowRouter.group {}
He, that was easy! The reason that we’re not calling the group ‘public’ is because that’s a reserved word in Coffeescript / Javascript. So we just call it exposed (or whatever name you want it to be).
Now lets make 2 routes:
- login is where users can login.
- signup is the route where people can sign up.
exposed.route ‘/login’,
name: ‘login’
action: ->
BlazeLayout.render “login”exposed.route ‘/signup’,
name: ‘signup’
action: ->
BlazeLayout.render “signup”
As you can see, each route starts with ‘exposed’, meaning they belong to that group. The route consists of a path (e.g. ‘/login’), a name (‘login’) and an action. In this case we render a template, but you can do any action you like.
TIP:
it’s a good idea to use names for routes, this way you refer to the name in your code instead of the path (using arillo:flow-router-helpers). Whenever you change the path, your code won’t break.
Also notice how easy it is recognise your public routes because of the ‘exposed’ prefix. Another advantage of using Flow Router’s groups.
2. Logged in routes
Now let’s create some routes that are only available for logged in users. How? We make another group, called ‘loggedIn’
loggedIn = FlowRouter.group
triggersEnter: [ ->
unless Meteor.loggingIn() or Meteor.userId()
route = FlowRouter.current() unless route.route.name is 'login'
Session.set 'redirectAfterLogin', route.path FlowRouter.go ‘login’
]
This time, the group consists of an enter trigger. This means that whenever someone enters a route in this group, the trigger will run before the route runs. In this case the trigger checks if the user is logging in or if the user is logged in already, because when the user is logged in Meteor.user() will exist. Otherwise, the user is redirected to the login page.
Notice we’re using FlowRouter.go(‘login’) . Using route names makes your code more maintainable, because when you decide to change the path, your code won’t break.
Also notice that for the ‘redirectAfterLogin we don’t use the route name, but the path. This way you can redirect the user while keeping the state in the url.
Now imagine that the user typed in ‘http://www.myapp.com/dashboard’ but wasn’t logged in? Then we save the route that the user wanted to go in Session.set ‘redirectAfterLogin’. We then redirect the user to the login page and use the Accounts.onLogin hook like this:
Accounts.onLogin ->
redirect = Session.get ‘redirectAfterLogin’ if redirect?
unless redirect is '/login'
FlowRouter.go redirect
This makes sure the user goes to the route that he wanted after he successfully logged in. Notice the check here for ‘/login’ route. I’ve added this check here because some async behaviour in either FlowRouter or onLogin hook can cause wrong redirect to the ‘ login’ page again. This explicit check solves that issue. Way to go!
Now let’s make some routes:
loggedIn.route ‘/dashboard’,
name: ‘dashboard’
action: ->
BlazeLayout.render “dashboard”loggedIn.route ‘/settings’,
name: ‘settings’
action: ->
BlazeLayout.render “settings”
So whenever a user goes to http://www.yourapp.com/dashboard or http://www.yourapp.com/profile the triggersEnter from the loggedIn group will run, before the route action runs. So when the user is not loggin in, or not logged in, the action will never run because we redirect the user to ‘login’.
It’s really that simple.
3. Admin routes
Now lets make some routes that are only available to users with specific rights, in this case admins. We make another group, called ‘admin’
admin = loggedIn.group
prefix: “/admin”
triggersEnter: [ ->
unless Roles.userIsInRole Meteor.user(), [ ‘admin' ]
FlowRouter.go FlowRouter.path('dashboard’)
]
So what happens here is that the admin group extends the loggedIn group. This means that whenever you try to access an admin route, the triggersEnter from the loggedIn will execute first. If the user is not logging in, or not logged in, he will be redirected to ‘login’ and never get to the admin group.
However, if the user is logged in, and tries to access an admin route, the triggersEnter from the admin group will execute. In this case the trigger checks if the user has admin rights (using the excellent meteor roles package). If the user is no admin, we redirect him to the dashboard. If the user is an admin, he can continue.
Also notice that we use prefix: “/admin” which means that any admin route will be prefixed with /admin.
UPDATE May 26th 2016
The Roles package actually depends on a subscription, which means that if the subscription is not ready the Roles.userIsInRole method will always return false. And then your route fails, because FlowRouter always runs no matter what. This happens in very specific cases, but it happens and your users will notice.
Fortunately there is now a fix. We can have full control over when FlowRouter initializes. So add below code to your client startup.coffee / startup.js. Make sure this is outside of your Meteor.startup() function. It makes FlowRouter wait, and then when the Roles subscription is ready, we make it run. Your authentication will never fail again.
Please note that while the router is waiting, your users are also waiting! So don’t subscribe to too much data. Fortunately the Roles data is very tiny so it’s a matter of milliseconds.
FlowRouter.wait()Tracker.autorun ->
# if the roles subscription is ready, start routing
# there are specific cases that this reruns, so we also check
# that FlowRouter hasn't initalized already if Roles.subscription.ready() and !FlowRouter._initialized
FlowRouter.initialize()
Now let’s make some routes:
admin.route ‘/billing’,
name: ‘billing’
action: ->
BlazeLayout.render ‘billing'admin.route ‘/users’,
name: ‘users’
action: ->
BlazeLayout.render ‘users’
So the resulting routes are:
http://www.myapp.com/admin/billing and http://www.myapp.com/admin/users
4. Log out & redirect
Lets also make sure that if the user logs out he is being redirected from the page he’s currently at. We simply make a route for this:
loggedIn.route ‘/logout’,
name: ‘logout’
action: ->
Meteor.logout ->
FlowRouter.go FlowRouter.path('login’)
Now attach this route to a logout link in your app, and the user will be redirected to the login page after logging out.
5. Log out other clients with proper redirect
What if you use Meteor.logoutOtherClients? This means that if the user logs in in a new client, the user should be logged out in other clients he’s logged in, and in those clients, he should be redirected from the page he’s currently at. Turns out this is quite simple to accomplish too.
Add the following to the onLogin hook
Accounts.onLogin -> # logout other clients
Meteor.logoutOtherClients()
Session.set ‘loggedIn’, true
In the loggedIn session variable we will store that the user is loggedIn on this client. So each client keeps a reference that the user is logged in. I’ll get to the reason for this in a sec.
Now implement the following code in your client startup script (in the ‘/clients/startup’ folder):
Tracker.autorun ->
if !Meteor.userId()
if Session.get(‘loggedIn’)
FlowRouter.go FlowRouter.path('login')
So this autorun function runs reactively whenever Meteor.userId() changes, (it works because Meteor.userId() is reactive).
If the client is being logged out by Meteor.logoutOtherClients(), then Meteor.userId() will not exists. However, we only want to redirect the user if he is logged in in the client, otherwise we’re also redirecting him when he’s not logged in. That’s why we check for the loggedIn session variable. So the logic here is, that if there’s no Meteor.userId() anymore, but just before the user was logged in, redirect him to ‘login’
Now open 2 browsers, log into one of them. Then go to the other browser and log in there. You’ll see that in the first browser the user is being logged out and redirected to the ‘login’ route. Exactly what we want.
Lets also make sure the logged out user can go back to the route he was, simply by logging in on that client again. Just change the code of the autorun to:
Tracker.autorun ->
if !Meteor.userId()
if Session.get(‘loggedIn’) # get and save the current route
route = FlowRouter.current()
Session.set ‘redirectAfterLogin’, route.path FlowRouter.go FlowRouter.path(‘login’)
This gets and saves the current route before the redirect. As soon as the user logs in again, he will be redirected to that route (via the loginHook that we’ve implemented in step 2).
Finishing touch: Not Found route
To finish this guide, lets add a not found route:
FlowRouter.notFound =
action: ->
BlazeLayout.render ‘notFound’
And there you go.
Just notice how simple routing is with Flow Router, and how easy it is to secure your routes. Once you’ve defined your groups, just add new routes with the prefix and they are automatically public, logged-in only or for admin users.
Of course, not being able to navigate to a route doesn’t mean your app is secure, you should also secure your methods and publications. But implementing this guide and Flow Router will provide a framework to secure your routes.