├── README.md ├── course.json ├── code ├── index.html └── app.js └── simple-auth.mdown /README.md: -------------------------------------------------------------------------------- 1 | # Simple AngularJS Authentication with JWT 2 | 3 | This repository contains the markdown and code for the [AngularJS JWT auth tutorial](https://thinkster.io/angularjs-jwt-auth) 4 | on Thinkster.io. Pull requests are more than welcome! 5 | -------------------------------------------------------------------------------- /course.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Simple AngularJS Authentication with JWT", 3 | "description": "This tutorial will show you how to build an AngularJS authentication system using JWTs and $http interceptors.", 4 | "url": "angularjs-jwt-auth", 5 | "twitter": "IAmMattGreen", 6 | "coverImage": "", 7 | "git": "https://github.com/emgeee/angular-jwt-auth-tutorial", 8 | 9 | "author": { 10 | "github": "emgeee" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /code/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Angular Auth 4 | 5 | 6 | 7 | 8 |

Simple Angular Auth - Thinkster

9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 | {{main.message}} 21 | 22 |


23 | shameless plug 24 | 25 | 26 | -------------------------------------------------------------------------------- /code/app.js: -------------------------------------------------------------------------------- 1 | ;(function(){ 2 | function authInterceptor(API, auth) { 3 | return { 4 | // automatically attach Authorization header 5 | request: function(config) { 6 | var token = auth.getToken(); 7 | if(config.url.indexOf(API) === 0 && token) { 8 | config.headers.Authorization = 'Bearer ' + token; 9 | } 10 | 11 | return config; 12 | }, 13 | 14 | // If a token was sent back, save it 15 | response: function(res) { 16 | if(res.config.url.indexOf(API) === 0 && res.data.token) { 17 | console.log('here') 18 | auth.saveToken(res.data.token); 19 | } 20 | 21 | return res; 22 | }, 23 | } 24 | } 25 | 26 | function authService($window) { 27 | var self = this; 28 | 29 | // Add JWT methods here 30 | self.parseJwt = function(token) { 31 | var base64Url = token.split('.')[1]; 32 | var base64 = base64Url.replace('-', '+').replace('_', '/'); 33 | return JSON.parse($window.atob(base64)); 34 | } 35 | 36 | self.saveToken = function(token) { 37 | $window.localStorage['jwtToken'] = token; 38 | } 39 | 40 | self.getToken = function() { 41 | return $window.localStorage['jwtToken']; 42 | } 43 | 44 | self.isAuthed = function() { 45 | var token = self.getToken(); 46 | if(token) { 47 | var params = self.parseJwt(token); 48 | return Math.round(new Date().getTime() / 1000) <= params.exp; 49 | } else { 50 | return false; 51 | } 52 | } 53 | 54 | self.logout = function() { 55 | $window.localStorage.removeItem('jwtToken'); 56 | } 57 | } 58 | 59 | function userService($http, API, auth) { 60 | var self = this; 61 | self.getQuote = function() { 62 | return $http.get(API + '/auth/quote'); 63 | } 64 | 65 | // add authentication methods here 66 | self.register = function(username, password) { 67 | return $http.post(API + '/auth/register', { 68 | username: username, 69 | password: password 70 | }) 71 | .then(function(res) { 72 | auth.saveToken(res.data.token); 73 | return res; 74 | }) 75 | } 76 | 77 | self.login = function(username, password) { 78 | return $http.post(API + '/auth/login', { 79 | username: username, 80 | password: password 81 | }) 82 | } 83 | } 84 | 85 | // We won't touch anything in here 86 | function MainCtrl(user, auth) { 87 | var self = this; 88 | 89 | function handleRequest(res) { 90 | var token = res.data ? res.data.token : null; 91 | if(token) { console.log('JWT:', token); } 92 | self.message = res.data.message; 93 | } 94 | 95 | self.login = function() { 96 | user.login(self.username, self.password) 97 | .then(handleRequest, handleRequest) 98 | } 99 | self.register = function() { 100 | user.register(self.username, self.password) 101 | .then(handleRequest, handleRequest) 102 | } 103 | self.getQuote = function() { 104 | user.getQuote() 105 | .then(handleRequest, handleRequest) 106 | } 107 | self.logout = function() { 108 | auth.logout && auth.logout() 109 | } 110 | self.isAuthed = function() { 111 | return auth.isAuthed ? auth.isAuthed() : false 112 | } 113 | } 114 | 115 | angular.module('app', []) 116 | .factory('authInterceptor', authInterceptor) 117 | .service('user', userService) 118 | .service('auth', authService) 119 | .constant('API', 'http://test-routes.herokuapp.com') 120 | .config(function($httpProvider) { 121 | $httpProvider.interceptors.push('authInterceptor'); 122 | }) 123 | .controller('Main', MainCtrl) 124 | })(); 125 | 126 | -------------------------------------------------------------------------------- /simple-auth.mdown: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | In this tutorial I'm going to take you through building a simple 4 | authentication mechanism for AngularJS apps using JWTs (JSON web tokens) 5 | combined with local storage. We'll build out a service for managing the tokens 6 | and create an `$http` interceptor for automatically attaching the tokens to 7 | requests. As this guide discusses front-end concepts only, I have built out 8 | a publicly accessible test API that you are encouraged to test your own code 9 | against. If you're interested in learning about the backend component, checkout 10 | our [MEAN tutorial](https://thinkster.io/mean-stack-tutorial/) or let me know 11 | [on twitter](https://twitter.com/IAmMattGreen)! 12 | 13 | {intro-video: angular-jwt-intro} 14 | 15 | # What are JWTs 16 | 17 | JWTs provide a way for clients to authenticate every request without having to 18 | maintain a session or repeatedly pass login credentials to the server. A JWT 19 | consists of three main components: a header object, a claims object, and 20 | a signature. These three properties are encoded using base64, then concatenated 21 | with periods as separators. Some important things to know about JWT's: 22 | 23 | - The claims object contains an expiration date which dictates how long the token is valid for 24 | - The claims object can also contain custom bits of information such as a user ID 25 | - The token is NOT encrypted so anyone with it can read all the properties 26 | - The token IS signed by the server so if any of the values are changed, the server will reject it 27 | 28 | {x: read atlassian guide} 29 | Read Atlassian's [understanding JWT](https://developer.atlassian.com/static/connect/docs/concepts/understanding-jwt.html) guide 30 | 31 | You can also find more information about JWT at [jwt.io](http://jwt.io). 32 | 33 | 34 | # The Setup 35 | 36 | {video: the-setup} 37 | 38 | Before we dive in, let's start with some scaffolding for our example. Create 39 | two new files -- index.html and app.js -- in an empty directory and add the 40 | following code: 41 | 42 | {x: create index.html} 43 | Create index.html 44 | 45 | ```html 46 | 47 | 48 | Simple Angular Auth 49 | 50 | 51 | 52 | 53 |

Simple Angular Auth - Thinkster

54 | 55 |
56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 |

65 | {{main.message}} 66 | 67 |


68 | shameless plug 69 | 70 | 71 | ``` 72 | 73 | {x: create app.js} 74 | Create app.js 75 | 76 | ```javascript 77 | ;(function(){ 78 | function authInterceptor(API, auth) { 79 | return { 80 | // automatically attach Authorization header 81 | request: function(config) { 82 | return config; 83 | }, 84 | 85 | // If a token was sent back, save it 86 | response: function(res) { 87 | return res; 88 | }, 89 | } 90 | } 91 | 92 | function authService($window) { 93 | var self = this; 94 | 95 | // Add JWT methods here 96 | } 97 | 98 | function userService($http, API, auth) { 99 | var self = this; 100 | self.getQuote = function() { 101 | return $http.get(API + '/auth/quote') 102 | } 103 | 104 | // add authentication methods here 105 | 106 | } 107 | 108 | // We won't touch anything in here 109 | function MainCtrl(user, auth) { 110 | var self = this; 111 | 112 | function handleRequest(res) { 113 | var token = res.data ? res.data.token : null; 114 | if(token) { console.log('JWT:', token); } 115 | self.message = res.data.message; 116 | } 117 | 118 | self.login = function() { 119 | user.login(self.username, self.password) 120 | .then(handleRequest, handleRequest) 121 | } 122 | self.register = function() { 123 | user.register(self.username, self.password) 124 | .then(handleRequest, handleRequest) 125 | } 126 | self.getQuote = function() { 127 | user.getQuote() 128 | .then(handleRequest, handleRequest) 129 | } 130 | self.logout = function() { 131 | auth.logout && auth.logout() 132 | } 133 | self.isAuthed = function() { 134 | return auth.isAuthed ? auth.isAuthed() : false 135 | } 136 | } 137 | 138 | angular.module('app', []) 139 | .factory('authInterceptor', authInterceptor) 140 | .service('user', userService) 141 | .service('auth', authService) 142 | .constant('API', 'http://test-routes.herokuapp.com') 143 | .config(function($httpProvider) { 144 | $httpProvider.interceptors.push('authInterceptor'); 145 | }) 146 | .controller('Main', MainCtrl) 147 | })(); 148 | ``` 149 | 150 | For simplicities sake we'll be working out of these two files for the remainder 151 | of this tutorial. 152 | 153 | {info} 154 | Because we'll be making cross-origin requests to the test API, make sure you're 155 | serving these files from a local webserver and NOT from the file system. 156 | I recommend http://www.browsersync.io/ (`npm install -g browser-sync`) 157 | 158 | 159 | # Working with JWTs 160 | 161 | Before we can really examine the JWT, we'll need to get one from the server. 162 | This involves registering or signing into an account. Then we'll build out 163 | our `auth` service for working with the token. 164 | 165 | ## Getting a Token 166 | 167 | {video: getting-token} 168 | 169 | There are two different actions a user can perform to identify themselves to 170 | the server (and thus receive a token): when a user first signs up and when the 171 | user logs in. We'll use a `user` service to handle these two interactions: 172 | 173 | {x: create user register} 174 | Create the `user.register()` method 175 | 176 | ```javascript 177 | self.register = function(username, password) { 178 | return $http.post(API + '/auth/register', { 179 | username: username, 180 | password: password 181 | }) 182 | } 183 | ``` 184 | 185 | {x: create user login} 186 | Create the `user.login()` method 187 | 188 | ```javascript 189 | self.login = function(username, password) { 190 | return $http.post(API + '/auth/login', { 191 | username: username, 192 | password: password 193 | }) 194 | }; 195 | ``` 196 | 197 | You can now create an account or authenticate against a previous account. If 198 | either action is successful, you should see a JWT printed out to the JavaScript 199 | console. 200 | 201 | {info} 202 | The test server is liable to clear account information at anytime so if 203 | a previously registered account no longer works, just create another one. 204 | 205 | 206 | Now that we can authenticate against the server and receive a token, we need to 207 | build out some functionality to parse, save, and retrieve the token 208 | information. To do this, we're going to use a separate service called `auth`. 209 | 210 | ## Decoding the Token 211 | 212 | {video: parsing-token} 213 | 214 | {x: decode claims object} 215 | As previously mentioned JWTs can contain custom properties in the claims 216 | object, which we will want to decode. Create a custom method in `authService` 217 | to do that: 218 | 219 | ```javascript 220 | self.parseJwt = function(token) { 221 | var base64Url = token.split('.')[1]; 222 | var base64 = base64Url.replace('-', '+').replace('_', '/'); 223 | return JSON.parse($window.atob(base64)); 224 | } 225 | ``` 226 | 227 | The test server includes three properties in the claims object: 228 | - `username` - should be the same you used when registering 229 | - `id` - allows the server to easily query the user from the database processing requests 230 | - `exp` - the expiration date in UNIX time 231 | 232 | {info} 233 | While you could store many attributes in the JWT, it's a good idea to 234 | keep the number to a minimum as the token must be sent with every request and 235 | larger tokens == larger requests. 236 | 237 | 238 | ## Persisting the Token 239 | 240 | {video: persisting-the-token} 241 | 242 | As long as the client possess a valid token, they can be considered 243 | "authenticated." We can persist this state across multiple page visits by 244 | storing the JWT using `localStorage`. 245 | 246 | {x: save token to localstorage} 247 | Add a method to save the token to `localStorage` thereby logging the user in: 248 | 249 | ```javascript 250 | self.saveToken = function(token) { 251 | $window.localStorage['jwtToken'] = token; 252 | } 253 | ``` 254 | 255 | {x: load the token from localstorage} 256 | We'll also want a method to retrieve the token from `localStorage`: 257 | 258 | ```javascript 259 | self.getToken = function() { 260 | return $window.localStorage['jwtToken']; 261 | } 262 | ``` 263 | 264 | ## Are we Authenticated? 265 | 266 | {video: isAuthed-method} 267 | 268 | Let's add a method that will check for the existence of a valid token and 269 | return true or false respectively. This method can be used throughout an 270 | application to show/hide elements or allow/disallow certain actions. 271 | 272 | {x: add isAuthed} 273 | Add the `isAuthed()` method: 274 | 275 | ```javascript 276 | self.isAuthed = function() { 277 | var token = self.getToken(); 278 | if(token) { 279 | var params = self.parseJwt(token); 280 | return Math.round(new Date().getTime() / 1000) <= params.exp; 281 | } else { 282 | return false; 283 | } 284 | } 285 | ``` 286 | 287 | {info} 288 | Remember that Unix Time is in seconds while JavaScript `Date.now()` returns 289 | milliseconds, so a conversion is necessary. 290 | 291 | ## Logging Out 292 | 293 | We'll round off the `auth` service with a way to log a user out. Since 294 | the presence of a valid JWT in `localStorage` is all that separates an 295 | authenticated user from an unathenticated user, logging out is simply a 296 | matter of deleting the token. 297 | 298 | {x: add logout method} 299 | Add a final method to the `auth` service to delete the stored JWT: 300 | 301 | ```javascript 302 | self.logout = function() { 303 | $window.localStorage.removeItem('jwtToken'); 304 | } 305 | ``` 306 | 307 | # Authenticating With an Interceptor 308 | 309 | Now that we have our JWT management in place, let's build out and HTTP 310 | interceptor to automatically send the token with each request that needs 311 | authentication. 312 | 313 | {x: read about interceptors} 314 | [Brush up on HTTP interceptors](https://thinkster.io/a-better-way-to-learn-angularjs/interceptors) 315 | 316 | 317 | ## Saving and Refreshing the Token 318 | 319 | {video: interceptor-token-save} 320 | 321 | At this point, we have created methods for saving and loading tokens but we 322 | haven't actually wired them up to do so. We can easily do 323 | this using the `response()` callback of our interceptor. Simply check each 324 | returning response from the API server for the presence of a `token` then 325 | call `auth.saveToken()`. 326 | 327 | {x: auto save token} 328 | Automatically save JWT tokens sent back from the server 329 | 330 | ```javascript 331 | response: function(res) { 332 | if(res.config.url.indexOf(API) === 0 && res.data.token) { 333 | auth.saveToken(res.data.token); 334 | } 335 | 336 | return res; 337 | } 338 | ``` 339 | 340 | Now your `register()` and `login()` calls should actually save the JWT without 341 | having to do anything else! 342 | 343 | One awesome result of using interceptors is the ability to automatically refresh 344 | the JWT. Anytime a user successfully makes an API call to a protected route, the 345 | server can attach a new token to the response payload. The interceptor will 346 | automatically grab the new token and save it so as long as the user continues 347 | to make requests to the server, the token will never expire and she won't be 348 | forced to re-authenticate. 349 | 350 | 351 | ## Attaching the Token 352 | 353 | {video: interceptor-attaching-token} 354 | 355 | Finally, let's attach the token to requests made to the API server. 356 | Authenticating with a JWT involves attaching an `Authorization` header to each 357 | request with a value consisting of the String "Bearer" and the JWT. This method 358 | relies on the use of SSL to prevent the token itself from being sniffed. 359 | 360 | {x: add authorization header} 361 | Use the `request()` function to automatically add the `Authorization` header 362 | to outgoing requests: 363 | 364 | ```javascript 365 | request: function(config) { 366 | var token = auth.getToken(); 367 | if(config.url.indexOf(API) === 0 && token) { 368 | config.headers.Authorization = 'Bearer ' + token; 369 | } 370 | 371 | return config; 372 | }, 373 | ``` 374 | 375 | {info} 376 | Notice how we're only adding the header to requests made to the API server. 377 | 378 | You should now be able to hit the "get quote" button and see a motivational 379 | quote... or Star Wars. 380 | 381 | {x: requests are authed} 382 | The requests are being authenticated 383 | 384 | 385 | 386 | # Wrapping Up 387 | 388 | {video: wrapping-up} 389 | 390 | JWTs provide a dead-simple way to authenticate users to your service. Because 391 | they don't depend on cookies or sessions, they work really well across multiple 392 | different platforms such as iOS or Android. 393 | 394 | The demo we constructed illustrates a fairly complete AngularJS implementation 395 | and is actually similar to the way that we do user authentication here at 396 | Thinkster. 397 | 398 | One case that is glaringly absent from this tutorial is handling automatic user 399 | redirection in the event of an expired/missing token being used. Because the 400 | sample app was so simple, it didn't make sense to implement this functionality 401 | but it's easy to do using the `responseError` function in the interceptor. 402 | Simply check all failed requests for error code 401 (Unathorized) that were 403 | directed to the API server. If it happens, redirect the user to the login page 404 | using. 405 | 406 | There are some interesting considerations when implementing the backend part 407 | of JWT authentication. [Our MEAN tutorial](https://thinkster.io/mean-stack-tutorial/) 408 | addresses some of these using the Express framework on Node but if you're 409 | interested in a more detailed follow-up to this tutorial 410 | [let me know on twitter!](https://twitter.com/IAmMattGreen) 411 | --------------------------------------------------------------------------------