MEAN Authentication with JWT (JSON Web Tokens)
Objectives |
---|
Compare and contrast a cookie-session and webtoken authentication |
Boostrap JWT authentication in a MEAN application |
Background
With Express and Ruby we learned the Cookie-Session method of authentication; however, there is a better way to do communicate authentication with Single Page Application and Service-Based Architecture. We're going to use an encrypted chunk of JSON called a JSON Web Token or JWT (pronounced ''jot'') to communicate authentication between client and server.
Reference auth0.com
Why Use JWT?
Aren't you tired of worrying about keeping track of all these things?
- Sessions - JWT doesn't require sessions
- Cookies - JWT you just save the token to the client
- CSRF - Send the JWT instead of a CSRF token
- CORS - Forget about it, if your JWT is valid, the data is on its way
Also these benefits:
- Speed - you don't have to look up the session
- Storage - you don't have to store the session
- Mobile Ready - Apps don't let you set cookies!
- Testing - you don't have to make loging in a special case in your tests
JWT Flow
Reference: blog.matoski.com
- Client logs in
- Client receives a token and stores it in localStorage or sessionStorage.
- Client does requests with the token using an AngularJS Interceptor
- Token gets decoded on the server
- Use token data to decide if user has access to the resource, otherwise return a 401 message
JWT FTW
A JWT is pretty easy to identify. It is three strings separated by .
aaaaaaaaaa.bbbbbbbbbbb.cccccccccccc
Each part has a different significance:
Here is a JWT Example:
Header
var header = {
"typ": "JWT",
"alg": "HS256"
}
Payload
var payload = {
"iss": "scotch.io",
"exp": 1300819380,
"name": "Chris Sevilleja",
"admin": true
}
Signature
var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
HMACSHA256(encodedString, 'secret');
REMEMBER The 'secret' acts as an encryption string known only by the two parties communicating via JWT. Protect your secrets!
JSON Web Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJleHAiOjEzMDA4MTkzODAsIm5hbWUiOiJDaHJpcyBTZXZpbGxlamEiLCJhZG1pbiI6dHJ1ZX0.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773
Further Reading
Features of mean-auth
User Service
The User service needs some custom routes:
app.factory('User', ['$resource', 'HOST', function ($resource, HOST) {
return $resource(HOST + '/api/users/:id', { id: '@id' }, {
update: { method: 'PUT' },
sign_up: { url: HOST + '/api/users', method: 'POST', isArray: false },
login: { url: HOST + '/api/users/login', method: 'POST', isArray: false },
logout: { url: HOST + '/api/users/logout', method: 'GET', isArray: false }
})
}])
Auth Service
So far we've used services like "client-side models" that connect to our RESTful API with $resource
, but you can also tuck away functions into services and an Authentication Service is a very nice group of functions.
app.factory('Auth', [function() {
return {
isLoggedIn: function () {
return !!localStorage.getItem('jwtToken')
}
}
}])
Angular Interceptors
An Angular interceptors allow you to "intercept" http requests and responses and change them. We use an interceptor to attach the JWT to every outgoing http request, and handle what to do with 401 (Unauthorized) statuses in any http response.
// SERVICES.JS
app.factory('authInterceptor', function ($rootScope, $q, $window) {
return {
request: function (config) {
config.headers = config.headers || {};
if ($window.localStorage.jwtToken) {
config.headers.Authorization = 'Bearer ' + $window.localStorage.jwtToken;
}
return config;
},
response: function (response) {
if (response.status === 401) {
// handle the case where the user is not authenticated
}
return response || $q.when(response);
}
};
})
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
});
$rootScope.$broadcast('taco') & $rootScope.$on('taco', callback)
Use AngularJS's $broadcast
and $on
to notify other controllers that you logged in:
$rootScope.$broadcast('loggedIn'); // TELL THE OTHER CONTROLLERS WE'RE LOGGED IN
$rootScope.$on('loggedIn', function () {
$scope.isLoggedIn = true
})
Challenges
Fork this mean-auth sample project and add a sign-up
method.
Goal Get the mean-auth app running locally
- Clone the mean-auth
$ git fork https://github.com/ajbraus/mean-auth $ cd mean-auth $ nodemon
Goal Add a sign-up feature
Add '/sign-up' template, route & controller
// app.js .when('/sign-up', { templateUrl: 'templates/sign-up' , controller: 'SignUpCtrl' })
- Add the email to the JWT
- Sign the JWT and send it back to the client and save it
- After signup go to '/'
- Notify the rest of the app that you are logged in with
$rootScope.$broadcast('loggedIn')
Goal: Hash passwords
- Install and
bcrypt
$ npm install --save bcrypt
Add bcrypt and salt to your
user.js
// user.js bcrypt = require('bcrypt'), salt = bcrypt.genSaltSync(10);
Add the
createSecure
,authenticate
andcheckPassword
functions to theUser
model.// // user.js // // ... UserSchema.statics.createSecure = function (email, password, callback) { // `this` references our schema // store it in variable `that` because `this` changes context in nested callbacks var that = this; // hash password user enters at sign up bcrypt.genSalt(function (err, salt) { bcrypt.hash(password, salt, function (err, hash) { console.log(hash); // create the new user (save to db) with hashed password that.create({ email: email, passwordDigest: hash }, callback); }); }); }; UserSchema.statics.authenticate = function (email, password, callback) { this.findOne({email: email}, function (err, user) { console.log(user); if (user === null) { callback('Can\'t find user with email ' + email, user); } else if (user.checkPassword(password)) { callback(null, user); } }); }; UserSchema.methods.checkPassword = function (password) { return password == this.password; };
- Use the
User.createSecure
method to create a user at signup. - Add the authenticate function to the
/api/users/login
routeUser.authenticate(req.body.email, req.body.password, function(error, user) { if (error) { res.send(error) } else if (user) { // CREATE, SIGN, AND SEND TOKEN HERE } });
Extra Credit: Validate that confirm password and password match in client Extra Credit: Submit a pull request back to mean-auth