Everyone hates passwords. I do and I especially hate debugging password-related issues for customers, clients, and parents.

A few apps, like Slack, Status Hero, and Ember Map use a magic link authentication strategy to eliminate the need at all for user login. Here’s how I accomplished this with JSON Web Tokens and good old fashioned email.

TLDR:

Use JSON web tokens Email a link with a redirect url with a ?token=<jwt> param Setup a “redirection” route and controller to parse query params Authenticate the token with a custom authenticator and refresh token in redirect route $$$

We were using JSON Web Tokens to handle our http-based authentication. JWT’s have the ability to encode arbitrary data within their payload, usually, user id, email address, or user role, but we can use this to include other data.

Security

A quick note on security - if a user forwards the email or somehow the email is otherwise intercepted, someone could use the token to takeover the user's account. Of course, this is the same issue with the forgot password flow, but understanding the security risks of your application and taking related security precautions is important. This is probably not a good choice for a banking or encrypted chat application.

Initial Steps

Assuming you’re using Ember Simple Auth and Ember Simple Auth Token (I’ll leave it as an exercise for the reader to handle the backend :))

Setup Route and Controller

In your ember app, setup a route and matching controller to handle the redirection and authentication logic. I called mine redirect with a path of r . So the url I want to send to the user is domain.com/r?token=<jwt> .

Controller Setup In your controller, define the query params:

//controllers/redirect.js export default Controller.extend({ queryParams: [’token’], token: null });

(Having to define redirects in a controller is one my biggest pains with Ember.)

Route (Part 1 of 2)

In the route, we’re going to make the magic (of magic login) work. Right now, we just need to setup some default boilerplate.

I never realized this, but query params are passed in the params argument in the route hook!

//routes/redirect.js export default Route.extend({ session: service(), currentUser: service(), model({ token }) { if (!token) { this.transitionTo(‘magiclogin’); } return false });

Right now, let’s return false from the model and transition the magic login email request route. Next, we’ll authenticate the request.

Authenticator

The expected flow for authenticating a session with Ember Simple Auth is to get your authenticator , then call the session ’s authenticate method against the defined authenticator and login credentials.

Using the traditional username/password flow for Ember Simple Auth Token, we’d do something like this:

// components/user-login.js actions: { login(username, password) { let authenticator = ‘authenticator:jwt’; this.get(‘session’) .authenticate(authenticator, {username, password}) .then(data => { //redirect or whatever }) .catch(err => {conosle.log(err)) } }

Ember simple auth will only call the authenticator’s authenticate method to validate the user’s session in the Ember app. But our session is already authenticated on the backend, so we just need to refresh the token to check if it’s still valid.

There wasn't a documented path forward.

So here’s the general overview of what we need to do:

Create a custom authenticator that takes a JWT as it’s argument In the .authenticate() method, extract the data from the token, then refresh the token to validate it on the server Handle the response from the server and complete our business logic

JWT-Login

So we’ll create an authenticator with ember generate authenticator jwt-login (You can pick your own name). Our strategy here will be to extend the existing jwt authenticator to get access to the underlying logic, and override it’s authenticate method .

With Javascript module loading, importing and extending the functionality now very easy.

// authenticators/jwt-login.js import jwt from ‘ember-simple-auth-token/authenticators/jwt’; export default jwt.extend({ authenticate(token) { return new Promise((resolve, reject) => { this.refreshAccessToken(token) .then(response => { let tokenData = this.handleAuthResponse(response); resolve(tokenData); }) .catch(err => { reject(err); }); }); }

Back to the Router (Part 2 of 2)

Now we need to put the jwt-login authenticator to work. Back in routes/redirect.js , we need to implement the login logic.

//routes/redirect.js export default Route.extend({ session: service(), currentUser: service(), model({ token }) { if (!token) { this.transitionTo(‘magiclogin’); } let authenticator = ‘authenticator:jwt-login’; return this.get(‘session’).authenticate(authenticator, token) });

Now we’re logged in and ready for action.