SPA authentication with Auth0

Notes on small, simple off the shelf login solution for SPA and server.

Sometimes it is nice to put a login on a small project without reinventing a wheel. Auth0 (I am unaffiliated with them) provides everything I need (and more) right out of the box. This blog post describes how to give a few users access to your single page application (SPA) and the corresponding server with minimum effort.

This blog post is based on two excellent Vue.js + Auth0 tutorials

These are just my own notes to clarify things that I expect to forget quickly when following the above two tutorials.

Goal

  • Have a small username / password database on Auth0, invite specific users by email, do not let people sign up
  • Restrict some routes in the SPA to authenticated users
  • Restrict some API end points to authenticated users who logged in through the web application

Auth0 setup

In Auth0 dashboard, add new client in https://manage.auth0.com/#/clients section. Choose "Single Page Application" type.

You will have three important pieces of information. First two pieces are necessary for the client side SPA code: Domain and Client ID. The third one is Client Secret and it will be needed by the server side to validate the token passed by the SPA to the server to make sure only the authorized users are making API requests.

Make sure to list all domains allowed to login in the callback list. For local testing, you could just add http://localhost:5000 or whatever port used when running locally. If you deploy to a platform, like Zeit Now, then give an additional widlcard domain (do NOT wildcard *.now.sh, instead you can wildcard specific application, for example).

1
2
http://localhost:5000
https://my-app-test0-*.now.sh/

In general, be careful with wildcards; what if someone registers an application with matching package name and deployed to now.sh? Maybe generate unique ID as the package name.

Go into "Connections" and make sure only "Username-Password-Authentication" is turned on. Then go into Connections / Database menu and select "Edit". Disable Sign Ups and limit the list of applications that can use this user database to the new SPA under "Clients" tab. If you want you can require stronger passwords by setting rules under "Password Policy" tab.

Create a few users under Users menu. You can give each user an initial password, but make sure the user is created in the right "Connection" database!

SPA code

We will keep track of the authenticated state in the Vue application, and we will use AuthLock module.

1
<script src="//cdn.auth0.com/js/lock/10.2/lock.min.js"></script>

The SPA will ask user to login when the Vue component is ready.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var App = Vue.extend({
data: {} {
return {
lock: new Auth0Lock(spaConfig.AUTH0_CLIENT_ID,
spaConfig.AUTH0_DOMAIN, spaConfig.lockOptions)
}
},
ready () {
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken)
})
this.lock.on('authorization_error', (error) => {
console.error(error)
})
}
})

The id_token contains the authentication token we receive from the Auth0. We can inspect it by pasting into https://jwt.io/.

We want to pass the id_token with each Ajax request to the server. We can do this by configuring Vue interceptor or by adding token on some API requests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
callSecretApi () {
const token = localStorage.getItem('id_token')
const jwtHeader = {
'Authorization': 'Bearer ' + token
}
const options = {
headers: jwtHeader
}
this.$http.get('/api/ping', options)
.then(...)
.catch(console.error)
}
}

Let us add a response interceptor that will make sure we handle "unauthorized" errors returned by the API. In that case, we can logout the user and return to the login screen immediately.

1
2
3
4
5
6
7
8
9
Vue.http.interceptors.push((req, next) => {
next(function (response) {
if (response.status === 401) {
this.logout()
router.go('/')
}
return response
})
})

In the above SPA initialization code we used client id and authentication domain. Where did we get the variable object spaConfig?

1
2
new Auth0Lock(spaConfig.AUTH0_CLIENT_ID,
spaConfig.AUTH0_DOMAIN, spaConfig.lockOptions)

We got these values from the server. Our page requested the configuration variables as a script spa-config.js.

1
2
3
4
5
6
7
<script src="//cdn.auth0.com/js/lock/10.2/lock.min.js"></script>
<div id="app"></div>
<script src="/spa-config.js"></script>
<script src="/vue/dist/vue.min.js"></script>
<script src="/vue-resource/dist/vue-resource.min.js"></script>
<script src="/vue-router/dist/vue-router.min.js"></script>
<script src="app.js"></script>

The script /spa-config.js is generated by js-to-js and allows avoiding injecting configuration values via inline scripts, making the web application more secure.

We can protect the routes by requiring authenticated user for some of them

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Utility to check auth status
function checkAuth () {
return !!localStorage.getItem('id_token')
}
// The public route can be viewed at any time
const Public = Vue.extend({
template: `<p>This is a public route</p>`
})
// The private route can only be viewed when
// the user is authenticated. The canActivate hook
// uses checkAuth to return true if the user is authenticated
// or false if not.
const Private = Vue.extend({
template: `<p>This is a private route</p>`,
route: {
canActivate () {
return checkAuth()
}
}
})
const router = new VueRouter({
history: true
})
router.map({
'/public': {
component: Public
},
'/private': {
component: Private
}
})
router.start(App, '#app')

We have protected our client side routes, and we are passing token with API request to let the server code check if the user is authorized.

Server code

The server code needs to serve the HTML and application JavaScript; it also needs to validate the id_token passed with API requests.

To validate the tokens, we need to use the "Auth0 client" application's "secret" value. The main details

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// server.js
const express = require('express')
const logger = require('morgan')
const jwt = require('express-jwt')
const jsToJs = require('js-to-js')
const version = require('version-middleware')

const SPA_CLIENT_SECRECT = '... client app secret ...'
const jwtCheck = jwt({
secret: SPA_CLIENT_SECRECT
})

var app = express()
app.use(logger('dev'))

// serve our static stuff
app.use(express.static(path.join(__dirname, 'public')))
app.use(express.static(path.join(__dirname, 'node_modules')))

// SPA variables
app.get('/spa-config.js',
jsToJs('spaConfig', {
AUTH0_CLIENT_ID: '... client id ...',
AUTH0_DOMAIN: '... app domain ...',
lockOptions: {
auth: {
params: {
scope: 'openid'
}
}
}
})
)

After this we should put public and private routes. Plus we need to handle the case when the token check fails.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// public route, for example version
app.get('/version', version())
// Verify Authorization header's Bearer token
app.use(jwtCheck)
// Below this point, everything is protected
app.get('/api/ping', function (req, res) {
res.send({status: 'ok'})
})
app.use(function (err, req, res, next) {
// handle token check failed case
if (err.name === 'UnauthorizedError') {
res.status(401).send('UnauthorizedError')
}
})

Cleaner separation of secured API calls

Some API calls might be public, for example I like having public /version route (using version-middleware). Thus the server can restrict which API calls are checked for valid token.

1
2
3
4
5
6
7
8
// Verify Authorization header's Bearer token
app.use('/secured', jwtCheck)
// Below this point, every call starting with /secured is protected
app.get('/secured/ping', function (req, res) {
res.send({status: 'ok'})
})
// public API route
app.get('/version', version())

On the client side we can add the token to every /secured Ajax call.

1
2
3
4
5
6
7
Vue.http.interceptors.push((req, next) => {
if (req.url.startsWith('/secured')) {
const token = localStorage.getItem('id_token')
req.headers['Authorization'] = 'Bearer ' + token
}
// response interceptor
})

Why not "audience"?

Auth0 has a different work flow that can connect nicely a client to a separate API. Unfortunately, this work flow does not work for Single Page Application clients yet. Thus I had to use the server with Client Secret to validate the tokens sent by the client. Please ensure the server - client connection is protected via SSL and the client code is secured against XSS attacks (by a strict Content-Security-Policy for example).