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 |
--------------------------------------------------------------------------------