Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feature: server to server example added
  • Loading branch information
jankapunkt committed Aug 18, 2023
commit c4245fb53b0b72f3be3a3da2de93f695bcbcb391
51 changes: 51 additions & 0 deletions server2server/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"ecmaFeatures" : {
"globalReturn": false,
"impliedStrict": true,
"jsx": false
}
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-var": [
"error"
],
"prefer-const": ["error", {
"destructuring": "any",
"ignoreReadBeforeAssign": false
}],
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"ignoreRestSiblings": false
}
]
}
}

157 changes: 157 additions & 0 deletions server2server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Server 2 server with Client Credentials Grant

## Architecture

The client credentials workflow is described in
[RFC 6749, section 4.4](https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.4):

```
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
```

In this setup the authorization server does also contain the consumable **resources**.
Therefore, the setup looks something like this:

```
+----------+ +--------------+
| | | |
| Consumer |>--(A)- Client Authentication --->| Provider |
| | | |
| |<--(B)---- Access Token ---------<| |
| | | |
| |>--(C)---- Access resource ---------> [Resource] |
| | | | |
| |<--(D)----Resource response -----<--------< |
+----------+ +--------------+
```

### Provider dependencies
- @node-oauth/express-oauth-server (uses @node-oauth/oauth2-server)
- express
- body-parser
- dotenv

### Consumer dependencies

- node-fetch
- dotenv

## Installation and usage

If you haven't already cloned this repository, then clone it via

```shell
$ git clone https://github.com/node-oauth/node-oauth2-server-examples.git
$ cd server2server
```

### Install and run the provider

Since we have two servers you need to install dependencies in both.
First, start with the provider:

```shell
$ cd provider
$ npm install
$ npm run start
```

The provider runs on `http://localhost:8080`


### Install and run the consumer

```shell
$ cd ../consumer
$ npm install
$ npm run start
```

The consumer will now make several requests. Note, that some of them are expected to fail.
The overall output should look like so:

```shell
[Consumer]: get /public (public)
[Consumer]: => response: 200 OK moo

[Consumer]: get /read-resource (not authenticated)
[Consumer]: => response: 401 Unauthorized

[Consumer]: post /token (bad credentials)
[Consumer]: => response: 401 Unauthorized {"error":"invalid_client","error_description":"Invalid client: client is invalid"}

[Consumer]: post /token (bad credentials)
[Consumer]: => response: 200 OK {"access_token":"45f81685482d6e1337b99ddb8726b7c04355b3d427b1401cf08e5c3bea013a38","token_type":"Bearer","expires_in":3600,"scope":true}

[Consumer]: authorization token successfully retrieved!

[Consumer]: get /read-resource (authenticated, resource is not yet defined)
[Consumer]: => response: 200 OK {"resource":null}

[Consumer]: post /write-resource (authentication failed)
[Consumer]: => response: 401 Unauthorized {"error":"invalid_token","error_description":"Invalid token: access token is invalid"}

[Consumer]: post /write-resource (Invalid token)
[Consumer]: => response: 200 OK {"message":"resource created"}

[Consumer]: get /read-resource (authenticated, resource is now)
[Consumer]: => response: 200 OK {"resource":"foo-bar-moo"}
```

## How routes are protected

If you take a look at the `provider/index.js` file, you will see a mix of public and private routes.

```js
app.get('/protected-route', oauth.authenticate(), function (req, res) {
res.send({ resource: internal.resource });
})
```

If the authenticated middleware fails, there will be no `next()` call into your provided handler function.
The authentication will fail, if no **access token** is provided.

In order to receive an access token, the client needs to make a call to the **public** `/token` endpoint, first:

```js
const tokenBodyParams = new URLSearchParams();
tokenBodyParams.append('grant_type', 'client_credentials');
tokenBodyParams.append('scope', 'read');

const response = await fetch('http://localhost:8080/token', {
method: 'post',
body: tokenBodyParams,
headers: {
'content-type': 'application/x-www-form-urlencoded',
'authorization': 'Basic ' + Buffer.from(`${client.id}:${client.secret}`).toString('base64'),
}
})
const token = await response.json()
const accessToken = token.access_token
const tokenType = token.token_type
```

With this token, the client can make authenticated requests to the protected endpoints:

```js
const response = await fetch('http://localhost:8080/read-resource', {
method: 'get',
headers: {
'authorization': `${tokenType} ${accessToken}`
}
})
const resource = await response.json()
```

Since there are no refresh token involved, the requests may fail, due to expired token.
It's up to the client to re-request a new token.

## License

MIT, see [LICENSE](../LICENSE) file
2 changes: 2 additions & 0 deletions server2server/consumer/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CLIENT_ID="this-client-id-is-for-demo-only"
CLIENT_SECRET="this-secret-id-is-for-demo-only"
110 changes: 110 additions & 0 deletions server2server/consumer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import fetch from 'node-fetch';

const rootUrl = 'http://localhost:8080';
const log = (...args) => console.log('[Consumer]:', ...args);
const getBody = async response => {
const body = await response.text();
log('=> response:', response.status, response.statusText, body, '\n');
return body;
};

const request = async ({ url, method = 'get', body, headers, note = '' }) => {
log(method, url, note && `(${note})`);
const fullUrl = `${rootUrl}${url}`;
const options = { method, body, headers };
const response = await fetch(fullUrl, options);
return getBody(response);
};

const run = async () => {
const client = {
id: process.env.CLIENT_ID,
secret: process.env.CLIENT_SECRET,
};

await request({
url: '/public',
note: 'public'
});

await request({
url: '/read-resource',
note: 'not authenticated'
});

const tokenBodyParams = new URLSearchParams();
tokenBodyParams.append('grant_type', 'client_credentials');
tokenBodyParams.append('scope', 'read');

await request({
url: '/token',
note: 'bad credentials',
method: 'post',
body: tokenBodyParams,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'authorization': 'Basic ' + Buffer.from('wrongId:wrongSecret').toString('base64'),
}
});

const body = await request({
url: '/token',
note: 'valid credentials',
method: 'post',
body: tokenBodyParams,
headers: {
'content-type': 'application/x-www-form-urlencoded',
'authorization': 'Basic ' + Buffer.from(`${client.id}:${client.secret}`).toString('base64'),
}
});


const token = JSON.parse(body);
const accessToken = token.access_token;
const tokenType = token.token_type;

if (accessToken && tokenType) {
log('authorization token successfully retrieved!', '\n');
}

await request({
url: '/read-resource',
note: 'authenticated, resource is not yet defined',
headers: {
'authorization': `${tokenType} ${accessToken}`
}
});

await request({
url: '/write-resource',
method: 'post',
note: 'authentication failed',
body: JSON.stringify({ value: 'foo-bar-moo' }),
headers: {
'content-type': 'application/json',
'authorization': `${tokenType} random-token-foo`
}
});

await request({
url: '/write-resource',
method: 'post',
note: 'Invalid token',
body: JSON.stringify({ value: 'foo-bar-moo' }),
headers: {
'content-type': 'application/json',
'authorization': `${tokenType} ${accessToken}`
}
});


await request({
url: '/read-resource',
note: 'authenticated, resource is now',
headers: {
'authorization': `${tokenType} ${accessToken}`
}
});
};

run().catch(console.error);
Loading