Published "A Tour of MPJ: Authentication" post

...and added links to intro and prior posts
This commit is contained in:
Daniel J. Summers 2018-08-30 09:36:13 -05:00
parent 2f9f5ca7ce
commit df8fba3fe3
3 changed files with 17 additions and 13 deletions

View File

@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "A Tour of myPrayerJournal: Authentication" title: "A Tour of myPrayerJournal: Authentication"
date: 2018-08-28 12:15:00 date: 2018-08-30 09:30:00
author: Daniel author: Daniel
categories: categories:
- [ Programming, JavaScript, Vue ] - [ Programming, JavaScript, Vue ]
@ -33,10 +33,10 @@ This decision has proved to be a good one. In the introduction, we mentioned all
## Integrating Auth0 in the App ## Integrating Auth0 in the App
JavaScript seems to be Auth0's primary language. They provide an [npm package][npm] to support using the responses that will be returned from their hosted login page. The basic flow is: JavaScript seems to be Auth0's primary language. They provide an [npm package][npm] to support using the responses that will be returned from their hosted login page. The basic flow is:
- The user clicks a link that executes their `authorize()` function - The user clicks a link that executes Auth0's `authorize()` function
- The user completes authorization through Auth0 - The user completes authorization through Auth0
- Auth0 returns the result and JWT to a predefined endpoint in the app - Auth0 returns the result and JWT to a predefined endpoint in the app
- The app uses their `parseHash()` function to extract the JWT from the URL (a `GET` request) - The app uses Auth0's `parseHash()` function to extract the JWT from the URL (a `GET` request)
- If everything is good, establish the user's session and proceed - If everything is good, establish the user's session and proceed
myPrayerJournal's implementation is contained in `AuthService.js` ([mpj:AuthService.js][AuthService.js]). There is a file that is not part of the source code repository; this is the file that contains the configuration variables for the Auth0 instance. Using these variables, we configure the `WebAuth` instance from the Auth0 package; this instance becomes the execution point for our other authentication calls. myPrayerJournal's implementation is contained in `AuthService.js` ([mpj:AuthService.js][AuthService.js]). There is a file that is not part of the source code repository; this is the file that contains the configuration variables for the Auth0 instance. Using these variables, we configure the `WebAuth` instance from the Auth0 package; this instance becomes the execution point for our other authentication calls.
@ -45,22 +45,22 @@ myPrayerJournal's implementation is contained in `AuthService.js` ([mpj:AuthServ
We'll start easy. The `login()` function simply exposes Auth0's `authorize()` function, which directs the user to the hosted log on page. We'll start easy. The `login()` function simply exposes Auth0's `authorize()` function, which directs the user to the hosted log on page.
The next in logical sequence, `handleAuthentication()`, is called by `LogOn.vue` ([mpj:LogOn.vue][LogOn.vue]) on line 16, passing in our store and the router. (In our last post, we discussed how server requests to a URL handled by the app should simply return the app, so that in can process the request; this is one of those cases.) `handleAuthentication()` does several things: The next in logical sequence, `handleAuthentication()`, is called by `LogOn.vue` ([mpj:LogOn.vue][LogOn.vue]) on line 16, passing in our store and the router. (In our [last post][], we discussed how server requests to a URL handled by the app should simply return the app, so that it can process the request; this is one of those cases.) `handleAuthentication()` does several things:
- It calls `parseHash()` to extract the JWT from the request's query string. - It calls `parseHash()` to extract the JWT from the request's query string.
- If we got both an access token and an ID token: - If we got both an access token and an ID token:
- It calls `setSession()`, which saves these to local storage, and schedules renewal (which we'll discuss more in a bit). - It calls `setSession()`, which saves these to local storage, and schedules renewal (which we'll discuss more in a bit).
- It then calls Auth0's `userInfo()` function to retrieve the user profile for the token we just received. - It then calls Auth0's `userInfo()` function to retrieve the user profile for the token we just received.
- When that comes back, it calls the store's ([mpj:store/index.js][store]) `USER_LOGGED_ON` mutation, passing the user profile; the mutation saves the profile to the store, local storage, and sets the `Bearer` token on the API service (more on that below as well). - When that comes back, it calls the store's ([mpj:store/index.js][store]) `USER_LOGGED_ON` mutation, passing the user profile; the mutation saves the profile to the store, local storage, and sets the `Bearer` token on the API service (more on that below as well).
- Finally, it replaces the current location (`/user/log-on?[lots-of-base64-stuff]`) with the URL `/journal`; this navigates the user to their journal. - Finally, it replaces the current location (`/user/log-on?[lots-of-base64-stuff]`) with the URL `/journal`; this navigates the user to their journal.
- If something didn't go right, we log to the console and pop up an alert. There may be a more elegant way to handle this, but in testing, the only way to reliably make this pop up was to mess with things. If you're messing with things, you don't get nice error messages. - If something didn't go right, we log to the console and pop up an alert. There may be a more elegant way to handle this, but in testing, the only way to reliably make this pop up was to mess with things behind the scenes. (And, if people do that, they're not entitled to nice error messages.)
Let's dive into the store's `USER_LOGGED_ON` mutation a bit more; it starts on line 68. The local storage item and the state mutations are pretty straightforward, but what about that `api.setBearer()` call? The API service ([mpj:api/index.js][api]) handles all the API calls through the [Axios][] library. Axios supports defining default headers that should be sent with every request, and we'll use the HTTP `Authorization: Bearer [base64-jwt]` header to tell the API what user is logged in. Line 18 sets the default `authorization` header to use for all future requests. (Back in the store, note that the `USER_LOGGED_OFF` mutation (just above this) does the opposite; it clears the `authorization` header. The `logout()` function in `AuthService.js` clears the local storage.) Let's dive into the store's `USER_LOGGED_ON` mutation a bit more; it starts on line 68. The local storage item and the state mutations are pretty straightforward, but what about that `api.setBearer()` call? The API service ([mpj:api/index.js][api]) handles all the API calls through the [Axios][] library. Axios supports defining default headers that should be sent with every request, and we'll use the HTTP `Authorization: Bearer [base64-jwt]` header to tell the API what user is logged in. Line 18 sets the default `authorization` header to use for all future requests. (Back in the store, note that the `USER_LOGGED_OFF` mutation (just above this) does the opposite; it clears the `authorization` header. The `logout()` function in `AuthService.js` clears the local storage.)
At this point, once the user is logged in, the `Bearer` token is sent with every API call. Each component, nor the store or its actions, need to do anything differently; it just works. At this point, once the user is logged in, the `Bearer` token is sent with every API call. None of the components, nor the store or its actions, need to do anything differently; it just works.
## Maintaining Authentication ## Maintaining Authentication
JWTs have short expirations, usually expressed in hours. Have a user's authentication go stale is not good! The `scheduleRenewal()` function in `AuthService.js` schedules a behind-the-scenes renewal of the JWT. When the time for renewal arrives, `renewToken()` is called, and if the renewal is successful, it runs the result through `setSession()`, just as we did above, which schedules the next renewal as its last step. JWTs have short expirations, usually expressed in hours. Having a user's authentication go stale is not good! The `scheduleRenewal()` function in `AuthService.js` schedules a behind-the-scenes renewal of the JWT. When the time for renewal arrives, `renewToken()` is called, and if the renewal is successful, it runs the result through `setSession()`, just as we did above, which schedules the next renewal as its last step.
For this to work, we had to add `/static/silent.html` as an authorized callback for Auth0. This is an HTML page that sits outside of the Vue app; however, the `usePostMessage: true` parameter tells the renewal call that it will receive its result from a `postMessage` call. `silent.html` uses the Auth0 library to parse the hash and post the result to the parent window.<a href="#note-2"><sup>2</sup></a> For this to work, we had to add `/static/silent.html` as an authorized callback for Auth0. This is an HTML page that sits outside of the Vue app; however, the `usePostMessage: true` parameter tells the renewal call that it will receive its result from a `postMessage` call. `silent.html` uses the Auth0 library to parse the hash and post the result to the parent window.<a href="#note-2"><sup>2</sup></a>
@ -68,9 +68,9 @@ For this to work, we had to add `/static/silent.html` as an authorized callback
Now that we're sending a `Bearer` token to the API, the API can tell if a user is logged in. We looked at some of the handlers that help us do that when we looked at the API in depth. Let's return to those and see how that is. Now that we're sending a `Bearer` token to the API, the API can tell if a user is logged in. We looked at some of the handlers that help us do that when we looked at the API in depth. Let's return to those and see how that is.
Before we look at the handlers, though, we need to look at the configuration, contained in `Program.fs` ([mpj:Program.fs][Program.fs]). You may remember that Giraffe sits atop ASP.NET Core; we can utilize it's `JwtBearer` methods to set everything up. Lines 38-48 are the interesting ones for us; we use the `UseAuthentication` extension method to set up JWT handling, then use the `AddJwtBearer` extension method to configure our specific JWT values. (As with the app, these are part of a file that is not in the repository.) The end result of this configuration is that, if there is a `Bearer` token that is a valid JWT, the `User` property of the `HttpContext` has an instance of the `ClaimsPrincipal` type, and the various properties from the JWT's payload are registered as `Claims` on that user. Before we look at the handlers, though, we need to look at the configuration, contained in `Program.fs` ([mpj:Program.fs][Program.fs]). You may remember that Giraffe sits atop ASP.NET Core; we can utilize its `JwtBearer` methods to set everything up. Lines 38-48 are the interesting ones for us; we use the `UseAuthentication` extension method to set up JWT handling, then use the `AddJwtBearer` extension method to configure our specific JWT values. (As with the app, these are part of a file that is not in the repository.) The end result of this configuration is that, if there is a `Bearer` token that is a valid JWT, the `User` property of the `HttpContext` has an instance of the `ClaimsPrincipal` type, and the various properties from the JWT's payload are registered as `Claims` on that user.
Now we can turn our attention to the handlers ([mpj:Handlers.fs][Handlers.fs]). `authorize`, on line 72, calls `user ctx`, which is defined on lines 50-51. All this does is look for a claim of the type `ClaimTypes.NameIdentifier`. This can be non-intuitive, as the source for this is the `sub` property from the JWT<a href="#note-3"><sup>3</sup></a>. A valid JWT with a `sub` claim is how we tell we have a logged on user. Now we can turn our attention to the handlers ([mpj:Handlers.fs][Handlers.fs]). `authorize`, on line 72, calls `user ctx`, which is defined on lines 50-51. All this does is look for a claim of the type `ClaimTypes.NameIdentifier`. This can be non-intuitive, as the source for this is the `sub` property from the JWT<a href="#note-3"><sup>3</sup></a>. A valid JWT with a `sub` claim is how we tell we have a logged on user; an authenticated user is considered authorized.
You may have noticed that, when we were describing the entities for the API, we did not mention a `User` type. The reason for that is simple; the only user information it stores is the `sub`. `Request`s are assigned by user ID, and the user ID is included with every attempt to make any change to a request. This eliminates URL hacking or rogue API posting being able to get anything meaningful from the API. You may have noticed that, when we were describing the entities for the API, we did not mention a `User` type. The reason for that is simple; the only user information it stores is the `sub`. `Request`s are assigned by user ID, and the user ID is included with every attempt to make any change to a request. This eliminates URL hacking or rogue API posting being able to get anything meaningful from the API.
@ -86,7 +86,7 @@ We now have a complete application, with the same user session providing access
<a name="note-2"><sup>2</sup></a> _This does work, but not indefinitely; if I leave the same browser window open from the previous day, I still have to sign in again. I very well could be "doing it wrong;" this is an area where I probably experienced the most learning through creating this project._ <a name="note-2"><sup>2</sup></a> _This does work, but not indefinitely; if I leave the same browser window open from the previous day, I still have to sign in again. I very well could be "doing it wrong;" this is an area where I probably experienced the most learning through creating this project._
<a name="note-3"><sup>3</sup></a> _I won't share how long it took me to figure out that `sub` mapped to that; let's just categorize it as "too long." With a Microsoft login, it's the only one that doesn't come across by its JWT name._ <a name="note-3"><sup>3</sup></a> _I won't share how long it took me to figure out that `sub` mapped to that; let's just categorize it as "too long." In my testing, it's the only claim that doesn't come across by its JWT name._
[intro]: /2018/a-tour-of-myprayerjournal/introduction.html "A Tour of myPrayerJournal: Introduction | The Bit Badger Blog" [intro]: /2018/a-tour-of-myprayerjournal/introduction.html "A Tour of myPrayerJournal: Introduction | The Bit Badger Blog"
@ -95,6 +95,7 @@ We now have a complete application, with the same user session providing access
[npm]: https://www.npmjs.com/package/auth0-js [npm]: https://www.npmjs.com/package/auth0-js
[AuthService.js]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/auth/AuthService.js "app/src/auth/AuthService.js | myPrayerJournal | GitHub" [AuthService.js]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/auth/AuthService.js "app/src/auth/AuthService.js | myPrayerJournal | GitHub"
[LogOn.vue]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/components/user/LogOn.vue "app/src/components/user/LogOn.vue | myPrayerJournal | GitHub" [LogOn.vue]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/components/user/LogOn.vue "app/src/components/user/LogOn.vue | myPrayerJournal | GitHub"
[last post]: /2018/a-tour-of-myprayerjournal/the-api.html "A Tour of myPrayerJournal: The API | The Bit Badger Blog"
[store]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/store/index.js "app/src/store/index.js | myPrayerJournal | GitHub" [store]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/store/index.js "app/src/store/index.js | myPrayerJournal | GitHub"
[api]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/api/index.js "app/src/api/index.js | myPrayerJournal | GitHub" [api]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/app/src/api/index.js "app/src/api/index.js | myPrayerJournal | GitHub"
[Axios]: https://www.npmjs.com/package/axios [Axios]: https://www.npmjs.com/package/axios

View File

@ -32,9 +32,10 @@ Recently, we released version 1.0 of [myPrayerJournal][], a minimalistic prayer
- **[Part 1: The Front End][part1]** - Vue components and routing - **[Part 1: The Front End][part1]** - Vue components and routing
- **[Part 2: State in the Browser][part2]** - Vuex and getting information from an API - **[Part 2: State in the Browser][part2]** - Vuex and getting information from an API
- **[Part 3: The API][part3]** - Giraffe and JSON web endpoints - **[Part 3: The API][part3]** - Giraffe and JSON web endpoints
- **Part 4: Authentication** - Auth0, using information in both app and API - **[Part 4: Authentication][part4]** - Auth0, using information in both app and API
- **Part 5: The Data Store** - EF Core backed by PostgreSQL, with the `DbContext` defined in F# - **Part 5: The Data Store** - EF Core backed by PostgreSQL, with the `DbContext` defined in F#
- **Part 6: Documentation** - GitHub Pages generated on each commit - **Part 6: Documentation** - GitHub Pages generated on each commit
- **Part 7: Conclusion** - Lessons learned and opinions based on the development experience
_(these will be linked once each post has been published)_ _(these will be linked once each post has been published)_
@ -61,6 +62,7 @@ Armed with these requirements, we will pick up next time with a look at the Vue
[part1]: /2018/a-tour-of-myprayerjournal/the-front-end.html "A Tour of myPrayerJournal: The Front End | The Bit Badger Blog" [part1]: /2018/a-tour-of-myprayerjournal/the-front-end.html "A Tour of myPrayerJournal: The Front End | The Bit Badger Blog"
[part2]: /2018/a-tour-of-myprayerjournal/state-in-the-browser.html "A Tour of myPrayerJournal: State in the Browser | The Bit Badger Blog" [part2]: /2018/a-tour-of-myprayerjournal/state-in-the-browser.html "A Tour of myPrayerJournal: State in the Browser | The Bit Badger Blog"
[part3]: /2018/a-tour-of-myprayerjournal/the-api.html "A Tour of myPrayerJournal: The API | The Bit Badger Blog" [part3]: /2018/a-tour-of-myprayerjournal/the-api.html "A Tour of myPrayerJournal: The API | The Bit Badger Blog"
[part4]: /2018/a-tour-of-myprayerjournal/authentication.html "A Tour of myPrayerJournal: Authentication | The Bit Badger Blog"
[Angular]: https://angular.io [Angular]: https://angular.io
[Aurelia]: https://aurelia.io [Aurelia]: https://aurelia.io
[Elm]: http://elm-lang.org [Elm]: http://elm-lang.org

View File

@ -50,7 +50,7 @@ We aren't done with routes just yet, though. Let's take a look at that `notFound
Giraffe uses the term "handler" to define a function that handles a request. Handlers have the signature `HttpFunc -> HttpContext -> Task<HttpContext option>` (aliased as `HttpHandler`), and can be composed via the `>=>` ("fish") operator. The `option` part in the signature is the key in composing handler functions. The `>=>` operator creates a pipeline that sends the output of one function into the input of another; however, if a function fails to return a `Some` option for the `HttpContext` parameter, it short-circuits the remaining logic.<a href="#note-2"><sup>2</sup></a> Giraffe uses the term "handler" to define a function that handles a request. Handlers have the signature `HttpFunc -> HttpContext -> Task<HttpContext option>` (aliased as `HttpHandler`), and can be composed via the `>=>` ("fish") operator. The `option` part in the signature is the key in composing handler functions. The `>=>` operator creates a pipeline that sends the output of one function into the input of another; however, if a function fails to return a `Some` option for the `HttpContext` parameter, it short-circuits the remaining logic.<a href="#note-2"><sup>2</sup></a>
The biggest use of that composition in myPrayerJournal is determining if a user is logged in or not. Authorization is also getting its own post, so we'll just focus on the yes/no answer here. The `authorized` handler (line 71) looks for the presence of a user. If it's there, it returns `next ctx`, where `next` is the next `HttpFunc` and `ctx` is the `HttpContext` it received; this results in a `Task<HttpContext option>` which continues to process, hopefully following the happy path and eventually returning `Some`. If the user is not there, though, it returns the `notAuthorized` handler, also passing `next` and `ctx`; however, if we look up to line 67 and the definition of the `notAuthorized` handler, we see that it ignores both `next` and `ctx`, and returns `None`. However, notice that this handler has some fish composition in it; `setStatusCode` returns `Some` (it has succeeded) but we short-circuit the pipeline immediately thereafter. The biggest use of that composition in myPrayerJournal is determining if a user is logged in or not. Authorization is also getting [its own post][auth], so we'll just focus on the yes/no answer here. The `authorized` handler (line 71) looks for the presence of a user. If it's there, it returns `next ctx`, where `next` is the next `HttpFunc` and `ctx` is the `HttpContext` it received; this results in a `Task<HttpContext option>` which continues to process, hopefully following the happy path and eventually returning `Some`. If the user is not there, though, it returns the `notAuthorized` handler, also passing `next` and `ctx`; however, if we look up to line 67 and the definition of the `notAuthorized` handler, we see that it ignores both `next` and `ctx`, and returns `None`. However, notice that this handler has some fish composition in it; `setStatusCode` returns `Some` (it has succeeded) but we short-circuit the pipeline immediately thereafter.
We can see this in use in the handler for the `/api/journal` endpoint, starting on line 137. Both `authorize` and the inline function below it have the `HttpHandler` signature, so we can compose them with the `>=>` operator. If a user is signed in, they get a journal; if not, they get a 403. We can see this in use in the handler for the `/api/journal` endpoint, starting on line 137. Both `authorize` and the inline function below it have the `HttpHandler` signature, so we can compose them with the `>=>` operator. If a user is signed in, they get a journal; if not, they get a 403.
@ -86,7 +86,7 @@ If this still doesn't make sense, perhaps this will help. The `Configure.kestrel
<p>&nbsp;</p> <p>&nbsp;</p>
That concludes our tour of the API for now, though we'll be looking at it again next time, when we take a deeper dive into authentication and authorization using Auth0. That concludes our tour of the API for now, though we'll be looking at it again next time, when we take a deeper dive into [authentication and authorization using Auth0][auth].
--- ---
@ -103,6 +103,7 @@ That concludes our tour of the API for now, though we'll be looking at it again
[Giraffe]: https://github.com/giraffe-fsharp/Giraffe "Giraffe | GitHub" [Giraffe]: https://github.com/giraffe-fsharp/Giraffe "Giraffe | GitHub"
[TR]: https://github.com/giraffe-fsharp/Giraffe.TokenRouter "Giraffe.TokenRouter | GitHub" [TR]: https://github.com/giraffe-fsharp/Giraffe.TokenRouter "Giraffe.TokenRouter | GitHub"
[Handlers.fs]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/api/MyPrayerJournal.Api/Handlers.fs "app/Handlers.fs | myPrayerJournal | GitHub" [Handlers.fs]: https://github.com/bit-badger/myPrayerJournal/blob/1.0.0/src/api/MyPrayerJournal.Api/Handlers.fs "app/Handlers.fs | myPrayerJournal | GitHub"
[auth]: /2018/a-tour-of-myprayerjournal/authentication.html "A Tour of myPrayerJournal: Authentication | The Bit Badger Blog"
[Suave]: https://suave.io [Suave]: https://suave.io
[ROP]: https://fsharpforfunandprofit.com/posts/recipe-part2/ "Railway oriented programming | F# for Fun and Profit" [ROP]: https://fsharpforfunandprofit.com/posts/recipe-part2/ "Railway oriented programming | F# for Fun and Profit"
[ROP-fish]: https://fsharpforfunandprofit.com/posts/recipe-part2/#an-alternative-to-bind "An alternative to bind | Railway oriented programming | F# for Fun and Profit" [ROP-fish]: https://fsharpforfunandprofit.com/posts/recipe-part2/#an-alternative-to-bind "An alternative to bind | Railway oriented programming | F# for Fun and Profit"