|
| 1 | +# Server 2 server with Client Credentials Grant |
| 2 | + |
| 3 | +## Architecture |
| 4 | + |
| 5 | +The client credentials workflow is described in |
| 6 | +[RFC 6749, section 4.4](https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.4): |
| 7 | + |
| 8 | +``` |
| 9 | ++---------+ +---------------+ |
| 10 | +| | | | |
| 11 | +| |>--(A)- Client Authentication --->| Authorization | |
| 12 | +| Client | | Server | |
| 13 | +| |<--(B)---- Access Token ---------<| | |
| 14 | +| | | | |
| 15 | ++---------+ +---------------+ |
| 16 | +``` |
| 17 | + |
| 18 | +In this setup the authorization server does also contain the consumable **resources**. |
| 19 | +Therefore, the setup looks something like this: |
| 20 | + |
| 21 | +``` |
| 22 | ++----------+ +--------------+ |
| 23 | +| | | | |
| 24 | +| Consumer |>--(A)- Client Authentication --->| Provider | |
| 25 | +| | | | |
| 26 | +| |<--(B)---- Access Token ---------<| | |
| 27 | +| | | | |
| 28 | +| |>--(C)---- Access resource ---------> [Resource] | |
| 29 | +| | | | | |
| 30 | +| |<--(D)----Resource response -----<--------< | |
| 31 | ++----------+ +--------------+ |
| 32 | +``` |
| 33 | + |
| 34 | +### Provider dependencies |
| 35 | +- @node-oauth/express-oauth-server (uses @node-oauth/oauth2-server) |
| 36 | +- express |
| 37 | +- body-parser |
| 38 | +- dotenv |
| 39 | + |
| 40 | +### Consumer dependencies |
| 41 | + |
| 42 | +- node-fetch |
| 43 | +- dotenv |
| 44 | + |
| 45 | +## Installation and usage |
| 46 | + |
| 47 | +If you haven't already cloned this repository, then clone it via |
| 48 | + |
| 49 | +```shell |
| 50 | +$ git clone https://github.com/node-oauth/node-oauth2-server-examples.git |
| 51 | +$ cd server2server |
| 52 | +``` |
| 53 | + |
| 54 | +### Install and run the provider |
| 55 | + |
| 56 | +Since we have two servers you need to install dependencies in both. |
| 57 | +First, start with the provider: |
| 58 | + |
| 59 | +```shell |
| 60 | +$ cd provider |
| 61 | +$ npm install |
| 62 | +$ npm run start |
| 63 | +``` |
| 64 | + |
| 65 | +The provider runs on `http://localhost:8080` |
| 66 | + |
| 67 | + |
| 68 | +### Install and run the consumer |
| 69 | + |
| 70 | +```shell |
| 71 | +$ cd ../consumer |
| 72 | +$ npm install |
| 73 | +$ npm run start |
| 74 | +``` |
| 75 | + |
| 76 | +The consumer will now make several requests. Note, that some of them are expected to fail. |
| 77 | +The overall output should look like so: |
| 78 | + |
| 79 | +```shell |
| 80 | +[Consumer]: get /public (public) |
| 81 | +[Consumer]: => response: 200 OK moo |
| 82 | + |
| 83 | +[Consumer]: get /read-resource (not authenticated) |
| 84 | +[Consumer]: => response: 401 Unauthorized |
| 85 | + |
| 86 | +[Consumer]: post /token (bad credentials) |
| 87 | +[Consumer]: => response: 401 Unauthorized {"error":"invalid_client","error_description":"Invalid client: client is invalid"} |
| 88 | + |
| 89 | +[Consumer]: post /token (bad credentials) |
| 90 | +[Consumer]: => response: 200 OK {"access_token":"45f81685482d6e1337b99ddb8726b7c04355b3d427b1401cf08e5c3bea013a38","token_type":"Bearer","expires_in":3600,"scope":true} |
| 91 | + |
| 92 | +[Consumer]: authorization token successfully retrieved! |
| 93 | + |
| 94 | +[Consumer]: get /read-resource (authenticated, resource is not yet defined) |
| 95 | +[Consumer]: => response: 200 OK {"resource":null} |
| 96 | + |
| 97 | +[Consumer]: post /write-resource (authentication failed) |
| 98 | +[Consumer]: => response: 401 Unauthorized {"error":"invalid_token","error_description":"Invalid token: access token is invalid"} |
| 99 | + |
| 100 | +[Consumer]: post /write-resource (Invalid token) |
| 101 | +[Consumer]: => response: 200 OK {"message":"resource created"} |
| 102 | + |
| 103 | +[Consumer]: get /read-resource (authenticated, resource is now) |
| 104 | +[Consumer]: => response: 200 OK {"resource":"foo-bar-moo"} |
| 105 | +``` |
| 106 | + |
| 107 | +## How routes are protected |
| 108 | + |
| 109 | +If you take a look at the `provider/index.js` file, you will see a mix of public and private routes. |
| 110 | + |
| 111 | +```js |
| 112 | +app.get('/protected-route', oauth.authenticate(), function (req, res) { |
| 113 | + res.send({ resource: internal.resource }); |
| 114 | +}) |
| 115 | +``` |
| 116 | + |
| 117 | +If the authenticated middleware fails, there will be no `next()` call into your provided handler function. |
| 118 | +The authentication will fail, if no **access token** is provided. |
| 119 | + |
| 120 | +In order to receive an access token, the client needs to make a call to the **public** `/token` endpoint, first: |
| 121 | + |
| 122 | +```js |
| 123 | +const tokenBodyParams = new URLSearchParams(); |
| 124 | +tokenBodyParams.append('grant_type', 'client_credentials'); |
| 125 | +tokenBodyParams.append('scope', 'read'); |
| 126 | + |
| 127 | +const response = await fetch('http://localhost:8080/token', { |
| 128 | + method: 'post', |
| 129 | + body: tokenBodyParams, |
| 130 | + headers: { |
| 131 | + 'content-type': 'application/x-www-form-urlencoded', |
| 132 | + 'authorization': 'Basic ' + Buffer.from(`${client.id}:${client.secret}`).toString('base64'), |
| 133 | + } |
| 134 | +}) |
| 135 | +const token = await response.json() |
| 136 | +const accessToken = token.access_token |
| 137 | +const tokenType = token.token_type |
| 138 | +``` |
| 139 | + |
| 140 | +With this token, the client can make authenticated requests to the protected endpoints: |
| 141 | + |
| 142 | +```js |
| 143 | +const response = await fetch('http://localhost:8080/read-resource', { |
| 144 | + method: 'get', |
| 145 | + headers: { |
| 146 | + 'authorization': `${tokenType} ${accessToken}` |
| 147 | + } |
| 148 | +}) |
| 149 | +const resource = await response.json() |
| 150 | +``` |
| 151 | + |
| 152 | +Since there are no refresh token involved, the requests may fail, due to expired token. |
| 153 | +It's up to the client to re-request a new token. |
| 154 | + |
| 155 | +## License |
| 156 | + |
| 157 | +MIT, see [LICENSE](../LICENSE) file |
0 commit comments