Forget passwords permanently using JWT's and emails to automatically login users
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:
?token=<jwt>
paramWe 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.
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.
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 :))
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.
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:
authenticator
that takes a JWT as it’s argument.authenticate()
method, extract the data from the token, then refresh the token to validate it on the serverSo 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);
});
});
}
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.