Students will be able to:
- Understand the use case of token authentication
- Encode/decode a JSON Web Token (JWT)
- Configure ui-router for authorization on the client
- Configure an Express app to provide JWTs
- Persist a JWT on the client
- Configure Angular to provide the JWT with each request
- Verify a JWT on the server
-
Learn about token-based authentication:
- Review of session-based auth.
- What are JSON Web Tokens (JWT)?
- Advantages of JWT-based authentication.
-
Review starter app
-
Improve the starter app:
- Configuring the routing to redirect to the login state when an anonymous user attempts to browse to a "protected" state.
- Hashing the password in a user MongoDB document.
-
Refactor from session-based to token-based authentication:
- On the Server:
- Create a JWT when a user logs in or signs up.
- Send the newly created token to the client.
- Verify JWTs sent by clients.
- In the Client (AngularJS):
- Persist the JWT provided by the server.
- Send the JWT when making requests to the server.
- On the Server:
Before we talk about token-based authentication, let's review one of the types of auth that you've already used, session-based authentication.
A JSON Web Token is a single string that plays the role of a "token".
The key points about a JWT are:
- The token can contain whatever custom data (called claims) we want to put in it.
- The token is cryptographically signed by the server when it is created so that if the token is changed in any way, it is considered invalid.
- The token is encoded, but not encrypted. It is encoded using a standard known as base64url so that it can be easily serialized across the internet or even be included in a URL's querystring. Some developers look at encoded data and think that it's content cannot be read - this is not the case, as you'll soon see.
Here is how a JWT is structured:
There is a great website dedicated to JWTs that explains in detail their format as well as has the ability to create them: https://jwt.io/
Allow me to take a JWT from the website and demonstrate the fact that the token can be easily decoded in the browser's console:
> var jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
> var payload = jwt.split('.')[1] // only interested in the payload
> window.atob(payload)
< "{"sub":"1234567890","name":"John Doe","admin":true}"
The
atob()
method decodes a base-64 encoded string andbtoa()
base-64 encodes a string.
Now that we know what they are, let's discuss why we might want to use them...
Here's a graphic contrasting sessions and tokens:
Sessions are stateful - they have to be maintained in a server's memory or a database. The more active users there are, the sessions there are to keep track of.
The key to token-based authentication is that it's stateless, meaning there is no state being stored on the server regarding the session/login.
A JSON web token is self-contained, it can itself contain the user's identity, etc. - there's no need to fetch the user from a database (an expensive operation).
The stateless nature of tokens allows the implementation of single sign-on (SSO) - where the same token can be used to access several different applications, for example, Google Mail, Google Calendar, etc.!
The JWT Todos app has an Angular front-end with the Materialize CSS framework and an Express/Mongoose backend.
The app currently uses session-based authentication.
Allow me to briefly demo it.
The Angular app lives in the public folder. Why?
Script are in the public/javascripts folder.
Templates for each client route are in public/templates folder.
The Materialize CSS framework and Angular are being loaded in index.ejs via CDNs in the <head>
.
All of the Angular app's components are being loaded above the closing </body>
tag.
There is a fade-in animation when transitioning to a new state. This is thanks to the ngAnimate
module and a touch of CSS.
The server app was created using the express-generator.
There is a single server non-API route (GET /
) responsible for returning the index.ejs view. This particular app continues to have it's index view a server template. This can be handy if you want to process the view using EJS prior to it being sent to the client, however, this app has no server processing and therefore, index.ejs could easily have been a static index.html - Where would index.html be stored?
Guess what, Jr. web developers would rarely be responsible for writing apps from scratch. It is probable that your early work as a developer will consist of maintaining and modifying existing applications.
In this spirit, today's lesson is designed to give you an opportunity to familiarize yourself with an existing app and then refactor it.
Pair up and explore the app as necessary to answer the following questions:
-
How many Angular controllers are there registered with the app module?
-
Where (file and line no.) is the navController instantiated?
-
How many states (client routes) are defined?
-
What controller is newly instantiated if the user clicks this link:
<a href="#!/home">Home</a>
? -
Where (file and line no.) is the following line of code and what does it do?
$urlRouterProvider.otherwise('/home');
-
When a user clicks Log Out in the nav bar, what code runs (controller & method)?
-
All CRUD on Todos is performed through which Angular component?
-
Let's say you have an Angular controller that needs access to the logged in user's email, what does the controller need to do?
-
As the starter code is currently written, can an anonymous user create a todo?
-
Is a user's password hashed, encrypted, or stored as cleartext in the database?
-
What code (file and lines) logs a user in when they sign up?
-
If a user document is sent to the client as JSON, the password will not be sent - why?
-
What code on the server is responsible for adding the currently logged in user to Express' request object?
Our amazing JWT Todos app's API is protected on the backend as it should be. However, users will be users, for example...
If we browse to http://localhost:3000/#!/new
and attempt to add a new todo when we're not logged in, as expected, the server will return a status code of 401 (Unauthorized)
, but our app will confuse the heck out of the user because it just sits there doing nothing.
Let's see how we can modify our Angular app to automatically redirect to the login state whenever an anonymous user attempts to browse to a "protected" state.
We're going to take advantage of two concepts to implement client-side authorization:
- The
$stateChangeStart
event, and - Custom properties on state definition objects
ui-router
raises several events that we can listen to.
One of the most useful is the $stateChangeStart
event, which is raised before a state is transitioned to.
In our event handler code, we can do whatever we want, including preventing the new state from being activated and going to a different state instead.
You may remember how we listened to events in a controller using the $scope.$on()
method. However, it's also possible to listen to events on $rootScope
as well.
The $rootScope.$on()
method can listen to any event it's interested in that is raised from anywhere within the Angular app.
A great place to register the listener is in the run()
method of our app's main module.
Let's stub up the listener in app.js and just console.log
some info for now:
angular.module('app', ['ui.router', 'ngAnimate', 'ngResource'])
.config(configRoutes)
.run(runBlock);
runBlock.$inject = ['$rootScope'];
function runBlock($rootScope) {
$rootScope.$on('$stateChangeStart', function(evt, toState) {
console.log(evt, toState);
});
}
Now the evt
and toState
objects logged out whenever the state changes.
Examining the toState
object shows that it contains the same properties that we defined using $stateProvider
in app.js.
As we've done before, we'll take advantage of the ability in JS to create properties at will on objects.
We could get really elaborate and define a property that contains roles like this:
.state('viewAllUsers', {
url: '/users',
templateUrl: 'templates/users.html',
controller: 'UsersController as usersCtrl',
acceptRoles: ['admin', 'owner']
})
You have total flexibility to define any properties you wish, however, it is recommended that if you have several custom properties they be created within a data
object property.
As amazing as the JWT Todos app is, it only needs a simple property like the following added to every state that requires a logged in user:
.state('todos', {
url: '/todos',
templateUrl: 'templates/todos/index.html',
controller: 'TodosController as todosCtrl',
loginRequired: true
})
.state('newTodo', {
url: '/new',
templateUrl: 'templates/todos/new.html',
controller: 'NewController as newCtrl',
loginRequired: true
});
Now back to the $stateChangeStart
event handler...
The logic is simple, if the state being moved to has a loginRequired: true
property and there is no logged in user, we will move to the login
state instead. Here's the code:
runBlock.$inject = ['$rootScope', '$state', 'UserService'];
function runBlock($rootScope, $state, UserService) {
$rootScope.$on('$stateChangeStart', function(evt, toState) {
if (toState.loginRequired && !UserService.isLoggedIn()) {
evt.preventDefault();
$state.go('login');
}
});
}
Don't forget to add the two additional services being injected.
If you want to avoid activating a state, you need to call the preventDefault()
method on the event object.
To test it, make sure that you're not logged in and try browsing to localhost:3000/#!/todos
or localhost:3000/#!/new
and you should end up viewing the login view.
Note that it would not be terribly difficult to advance the above concept to create a custom message and actually transition to the original state the user was trying to transition to after they log in. Implementing this would be an excellent exercise for you to look into if you're interested.
The second way we can improve the starter code before refactoring it to use JWTs is to salt and hash the users' password when they sign up.
So off to the server-side JavaScript we go...
Currently, the password is being stored in the database as cleartext (plain text).
We've already leveraged MongooseJS middleware on the User
model's schema to ensure that a user's password is not included when serialized to JSON.
Once again, we will take advantage of Mongoose middleware to salt and hash the password whenever a user instance is being saved and the password has changed (including when a user is being created for the first time).
To perform the actual salting and hashing, we will use the ever so popular bcrypt
library - let's install and save it as a dependency:
$ npm install bcrypt --save
All of our new code will be added inside the file for the User
model (models/user.js).
First, bring in bccypt:
var mongoose = require('mongoose');
var bcrypt = require('bcrypt');
bcrypt has a setting that tells it how many times to randomize the generation of salt. Let's add a constant in the module to set it - usually 6 is enough:
var mongoose = require('mongoose');
var bcrypt = require('bcrypt');
const SALT_ROUNDS = 6;
Now for the middleware. We will be writing a function that runs before a user is saved. This is called pre middleware, also known as a "hook".
Type in this skeleton just above the module.exports
:
userSchema.pre('save', function(next) {
// this will be set to the current document
var user = this;
});
// new code above
module.exports = mongoose.model('User', userSchema);
Note that we are assigning this
to a variable.
?: Why are we doing this?
Now let's add the code that checks if the password
for this user document has been changed, and if so, salt & hash it, then assign the hash to password
, replacing the cleartext version:
userSchema.pre('save', function(next) {
var user = this;
if (!user.isModified('password')) return next();
// password has been changed - salt and hash it
bcrypt.hash(user.password, SALT_ROUNDS, function(err, hash) {
if (err) return next(err);
// override the user provided password with the hash
user.password = hash;
next();
});
});
Make sure that the server is running without error, then let's use Postman to test our model by creating a new user:
- What HTTP verb will we use?
- What URL will we use?
- Where do we look to find what needs to be included in the data payload?
The response in Postman will show us the newly created user, however, we have coded the model not to return the password - it's working!
We can use the Mongo Shell to check that the password has been hashed:
$ mongo
> use todos-with-auth
> db.users.find({})
--> user's password should be hashed!
Awesome!
Now we're going to have to refactor our login code to account for the fact that we've used bcrypt to salt & hash the password.
Luckily for us, bcrypt includes a compare
method for verifying that a cleartext password matches a given hash.
Now, we could just modify our controller's login
method by adding bcrypt, etc., however, it would be a better practice to put most of the new code in the User
model.
?: Why would this be a better practice?
When we want to add custom functionality to a particular instance of a Mongoose model, we can define instance methods like this:
userSchema.methods.comparePassword = function(tryPassword, cb) {
bcrypt.compare(tryPassword, this.password, function(err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
});
};
We can create any instance method to a model we want by adding it to the schema's methods object.
Now we need to refactor the login
method in the users controller to take advantage of the functionality we just added to our model:
function login(req, res, next) {
User.findOne({email: req.body.email}).exec().then(user => {
if (!user) return res.status(401).json({err: 'bad credentials'});
user.comparePassword(req.body.password, (err, isMatch) => {
if (isMatch) {
// using sessions to "remember" the logged in user's _id
req.session.userId = user._id;
res.json(user);
} else {
return res.status(401).json({err: 'bad credentials'});
}
});
}).catch(err => res.status(401).json(err));
}
In the above code, we've refactored the login
method to use the newly added comparePassword
method that's now available on every user document.
Use the JWT Todos app to test logging in!
As the application stands, it has a very nice implementation of session-based authentication, including both client & server authorization.
Now it's time to learn implement token-based authentication...
Here's what flow looks like when we use token-based auth:
Again, as a reminder, this processes is stateless on the server - there are no sessions tracking us!
Refactoring will consist of these steps on the server and client:
- On the server, create a JWT when a user logs in and return it to the client.
- On the client, persist the JWT provided by the server.
- On the client, send the JWT with each request to the server.
- On the server, verify the JWT sent by the client.
First, we're going to need to install the Node module that can create and verify JWTs.
https://jwt.io lists libraries available for your programming language of choice.
Let's install the one for Node apps:
$ npm install jsonwebtoken --save
Note: There are additional modules that you can use to help implement JWTs in Express apps. However, we want to give you your money's worth :)
With it installed, controllers/users.js is where we're going to be needing it:
var User = require('../models/user');
var jwt = require('jsonwebtoken');
As we saw, creating a JWT requires a "secret" string. Let's define one in our .env file:
DATABASE_URL=mongodb://localhost/todos-with-auth
SECRET=WDIRocks!
Then let's create a shortcut variable in our controller to hold it:
var User = require('../models/user');
var jwt = require('jsonwebtoken');
var SECRET = process.env.SECRET;
The library has a sign
method that creates JWTs. It can be used like this:
var token = jwt.sign(
<data payload object>,
<secret string>,
{expiresIn: '24h'} // one way of specifying the expiration
);
Now, let's refactor the login
method to create and return a JWT with the user included in the payload:
function login(req, res, next) {
User.findOne({email: req.body.email}).exec().then(user => {
if (!user) return res.status(401).json({err: 'bad credentials'});
user.comparePassword(req.body.password, (err, isMatch) => {
if (isMatch) {
var token = jwt.sign(
{user: user},
SECRET,
{expiresIn: '24h'}
);
// send token to client in Authorization header
res.set('Authorization', token);
res.json({msg: 'logged in successfully'});
} else {
return res.status(401).json({err: 'bad credentials'});
}
});
}).catch(err => res.status(401).json(err));
}
We could have returned the token as JSON, however, putting it in a header will allow us to send an "updated" token to the client with any response. For example, this can be used to implement a "sliding" expiration.
Note: There is no standard header name to use to send a token to a client, we, and others, have chosen to use the name
Authorization
.
The above refactoring will of course break our Angular app, but we can inspect the network request to ensure that we are receiving a JWT in the headers when we attempt to login.
Since the server may send us a new token in the headers in any response, we need to examine the headers in every response.
This could be complicated, but Angular provides with a cool approach, HTTP Interceptors...
HTTP Interceptors in Angular allow us to intercept and act upon every:
- HTTP request
- HTTP response
- HTTP request error
- HTTP response error
An HTTP Interceptor is a factory (service) that returns an object with any or all of the following methods:
-
request
Right before every
$http
request is sent to the server, this method is called with an HTTPconfig
object as an argument (yes, the same one that$http
uses). The method is free to modify or completely replace theconfig
object and then return it. -
response
When a server responds to a
$http
request, this method is called with the$http
response
object before it is passed to the.then
success function. The interceptor method is free to modifyresponse
object, then must return it. -
requestError
&responseError
These two methods get called if a request or response interceptor throws an error or if the
$http
service's errorCallback is called.
Okay, we're going to need to write a factory for the interceptor.
Create a file for it: $ touch public/javascripts/services/auth-interceptor.js
We created a script file, what do we need to do?
Let's start with the basic structure of the interceptor:
angular.module('app')
.factory('AuthInterceptor', AuthInterceptor);
AuthInterceptor.$inject = [];
function AuthInterceptor() {
return {
// Add request, response, requestError and/or responseError methods
}
}
Since we are interested in checking every response, we will want to write a response method like this:
function AuthInterceptor() {
return {
// Add request, response, requestError and/or responseError methods
response: function (response) {
var token = response.headers('Authorization');
console.log(token);
return response;
}
}
}
Notice that the response object has a headers()
method that returns the value for a header argument (or null
if the header does not exist). For now, we're just going to log out the token.
To test this, we need to actually add our interceptor to the $http
pipeline. The best place to do this is in the module's config method (yes, the same method where we defined our routing).
In app.js we first need to inject the $httpProvider
service then push our interceptor into the interceptors
array:
configRoutes.$inject = ['$stateProvider', '$urlRouterProvider', '$httpProvider'];
function configRoutes($stateProvider, $urlRouterProvider, $httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
// routes/states defined below...
Don't forget to refresh your app before logging in and ensuring that the Authorization
header is there.
Cool - but we still have work to do...
If there's an Authorization
header, we will want to take the token and put it in localStorage
so that we can access it whenever we need it.
Note: Data saved in
localStorage
is persisted by domain until removed. If you want to save data for only the duration of the browser session, usesessionStorage
instead.
Using localStorage
allows us to remain logged in until the token expires. We will be logged in, even if we close the browser and come back tomorrow!
We're going to put the code that sets and retrieves the token from localStorage
in it's own service. This extra step may seem unnecessary after we saw how easy it is to use localStorage
, however, the main benefit will be that we can check the expiration of the token before returning it - after all, if a token has expired, it's as if we don't have one.
Let's create a file for our token service: $ touch public/javascripts/services/token-service.js
What's next?
Now, let's write our TokenService
:
angular.module('app')
.factory('TokenService', TokenService);
TokenService.$inject = [];
function TokenService() {
var service = {
setToken: setToken
};
function setToken(token) {
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
}
return service;
}
Just a setToken
method for now. We'll add a other methods in a bit.
Okay, back to the interceptor, update the response
method to this:
response: function (response) {
var token = response.headers('Authorization');
if (token) TokenService.setToken(token);
return response;
}
Refresh the app and go to the Local Storage within the Application tab DevTools. Log in, and now you should have your token in localStorage!
Currently, our UserService
relies on a newly logged in user being returned as a JSON response. It then stores that user in the user
variable.
Now that we're storing our user within the token in localStorage, we need to refactor our code.
First, remove the var user = null;
at around line 8.
Also, we no longer have to make an initial call to the server to fetch the user's info - Why?.
Adios code:
// get logged in user if already exists in server session
$http.get('/api/users/me').then(function(res) {
user = res.data;
});
We're going to need use the TokenService
, so inject it:
userService.$inject = ['$http', 'TokenService'];
function userService($http, TokenService) {
...
Let's write a getUserFromToken()
helper function that will decode and extract the user object from the token. Add the helper function below the current return service;
line:
return service;
// helper functions
function getUserFromToken () {
var token = TokenService.getToken();
return token ? JSON.parse(atob(token.split('.')[1])).user : null;
}
We just called a TokenService.getToken()
method that does not yet exist.
But we'll want to write this method to ensure that it only returns a token if it has not expired yet.
Let's add the getToken
method to our TokenService
with this code:
function TokenService() {
var service = {
getToken: getToken,
setToken: setToken
};
function getToken() {
var token = localStorage.getItem('token');
if (token) {
// check if expired, remove if it is
var payload = JSON.parse(atob(token.split('.')[1]));
// JWT's exp is expressed in seconds, not milliseconds, so convert Date.now()
if (payload.exp < Date.now() / 1000) {
localStorage.removeItem('token');
token = null;
}
}
return token;
}
function setToken(token) {
...
Note: We needed to divide
Date.now()
by 1000. This is because the JWT spec says theexp
claim should be in Unix time - Unix Time is number of seconds since the Unix epoch (Jan 1, 1970). However, JS returns the number of milliseconds (not seconds) since the Unix epoch. We therefore must divide by 1000 to convert milliseconds to seconds.
Cool, back to the UserService
...
Thanks to the interceptor and other code we wrote, the login
method reduces to a single line of code:
function login(credentials) {
return $http.post('/api/users/login', credentials);
}
The getUser
and the isLoggedIn
methods refactor to this:
function getUser() {
return getUserFromToken();
}
function isLoggedIn() {
return !!getUserFromToken();
}
Logging out will require that we remove the token from localStorage, but this should be done in the TokenService
.
Add the following method to TokenService
:
...
var service = {
removeToken: removeToken,
getToken: getToken,
setToken: setToken
};
function removeToken() {
localStorage.removeItem('token');
}
...
Now we can refactor the logout
method to look like this:
function logout() {
TokenService.removeToken();
}
and because logging out is now synchronous, the NavController
's logout
method changes to:
function NavController($state, UserService) {
var vm = this;
vm.logout = function() {
UserService.logout();
$state.go('home');
};
...
Almost done with this step, except that we have to refactor signing up a new user also.
In the server's controllers/users.js file, refactor the create
method like this:
function create(req, res, next) {
User.create(req.body).then(user => {
var token = jwt.sign(
{user: user},
SECRET,
{expiresIn: '24h'}
);
res.set('Authorization', token);
res.json({msg: 'signed up successfully'});
}).catch( err => res.status(400).json(err) );
}
Lastly, back to UserService
:
function signup(userData) {
return $http.post('/api/users', userData);
}
Whew, that was a big step, literally!
The server is going to want to verify a valid JWT to access the protected routes in the API.
If we are logged in, we want to ensure that we send our JWT in a header...
So, once again, we can leverage HTTP Interceptors to automatically create a header in the request if we have a token to send.
We already have our AuthInterceptor
processing every response, now let's add a request
method to process every request and send our token (if we have one):
function AuthInterceptor(TokenService) {
return {
request: function(config) {
var token = TokenService.getToken();
if (token) config.headers.Authorization = 'Bearer ' + token;
return config;
},
...
We are pre-pending the word Bearer to our token. This is a standard to follow when using token-based authentication.
Again, refresh the app and log in. The app automatically makes sends an AJAX request to localhost:3000/api/todos
after logging in - this request will error (displayed in red) because we have not refactored our server yet, however, we can use DevTools to inspect the request and verify that the Authorization
header has been set with our token (with Bearer pre-pended).
On to the server!
Currently, the server is checking the session for userId
. Our goal is to eliminate sessions and use tokens instead.
The starter code already has an Express middleware function that fetched the user from the database using the session.userId
. We will refactor this middleware to take advantage of tokens instead.
Remember, the token will already contain the user's info, so we won't have to hit the database, we can use the token's info instead! No session, no querying the database - that's scalability!
Here's the refactored config/auth.js middleware:
var User = require('../models/user');
var jwt = require('jsonwebtoken');
var SECRET = process.env.SECRET;
module.exports = function(req, res, next) {
var token = req.body.token || req.query.token || req.get('Authorization');
if (token) {
// remove the 'Bearer ' if it was included in the token header
token = token.replace('Bearer ', '');
// check if token is valid and not expired
jwt.verify(token, SECRET, function(err, decoded) {
if (!err) {
// valid token, so add user to req
req.user = decoded.user;
next();
}
});
} else {
next();
}
}
Don't forget to add the two new require
modules at the top.
Note that we are checking for a token being sent in the request in three different ways by a client:
- In the header (this is what we are already doing)
- In the querystring, or
- In the body (
bodyParser
middleware has been configured to check both JSON and a form post)
This will allow our API to be accessed from other apps, mobile devices, etc.
Testing out the app and it appears that we've done it - congrats!!!!
Any code having to do with sessions is useless at this point.
To prove we don't need server-side sessions anymore, clean up the code to eliminate any trace of them.
Hint: There aren't really too many lines left, and they live in only one file.
Token-based authentication allows us to implement a "sliding" expiration.
This is when you stay logged in as long as you continue to interact with the app.
Hint: You will want to issue a new token on every request and this can be accomplished by adding some code to middleware we already have.
Be sure to test it out by issuing all JWTs with a very short expiration - like maybe 1m
.
At this point in the lesson, you would have duplicated the code that creates a token and puts it into a header on the response (res
) object - three times.
That's not very DRY!
If would be sweet to refactor the code into a separate module that you can require as needed.
There is a nice library for sliding notifications called toastr.
It has lots of options and is pretty easy to use - highly recommended for adding polish to SPAs.