Skip to content

Commit c4245fb

Browse files
committed
feature: server to server example added
1 parent 984fffc commit c4245fb

File tree

12 files changed

+2805
-0
lines changed

12 files changed

+2805
-0
lines changed

server2server/.eslintrc

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"extends": "eslint:recommended",
3+
"env": {
4+
"node": true,
5+
"mocha": true,
6+
"es6": true
7+
},
8+
"parserOptions": {
9+
"ecmaVersion": 9,
10+
"sourceType": "module",
11+
"ecmaFeatures" : {
12+
"globalReturn": false,
13+
"impliedStrict": true,
14+
"jsx": false
15+
}
16+
},
17+
"rules": {
18+
"indent": [
19+
"error",
20+
2
21+
],
22+
"linebreak-style": [
23+
"error",
24+
"unix"
25+
],
26+
"quotes": [
27+
"error",
28+
"single"
29+
],
30+
"semi": [
31+
"error",
32+
"always"
33+
],
34+
"no-var": [
35+
"error"
36+
],
37+
"prefer-const": ["error", {
38+
"destructuring": "any",
39+
"ignoreReadBeforeAssign": false
40+
}],
41+
"no-unused-vars": [
42+
"error",
43+
{
44+
"vars": "all",
45+
"args": "none",
46+
"ignoreRestSiblings": false
47+
}
48+
]
49+
}
50+
}
51+

server2server/README.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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

server2server/consumer/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CLIENT_ID="this-client-id-is-for-demo-only"
2+
CLIENT_SECRET="this-secret-id-is-for-demo-only"

server2server/consumer/index.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import fetch from 'node-fetch';
2+
3+
const rootUrl = 'http://localhost:8080';
4+
const log = (...args) => console.log('[Consumer]:', ...args);
5+
const getBody = async response => {
6+
const body = await response.text();
7+
log('=> response:', response.status, response.statusText, body, '\n');
8+
return body;
9+
};
10+
11+
const request = async ({ url, method = 'get', body, headers, note = '' }) => {
12+
log(method, url, note && `(${note})`);
13+
const fullUrl = `${rootUrl}${url}`;
14+
const options = { method, body, headers };
15+
const response = await fetch(fullUrl, options);
16+
return getBody(response);
17+
};
18+
19+
const run = async () => {
20+
const client = {
21+
id: process.env.CLIENT_ID,
22+
secret: process.env.CLIENT_SECRET,
23+
};
24+
25+
await request({
26+
url: '/public',
27+
note: 'public'
28+
});
29+
30+
await request({
31+
url: '/read-resource',
32+
note: 'not authenticated'
33+
});
34+
35+
const tokenBodyParams = new URLSearchParams();
36+
tokenBodyParams.append('grant_type', 'client_credentials');
37+
tokenBodyParams.append('scope', 'read');
38+
39+
await request({
40+
url: '/token',
41+
note: 'bad credentials',
42+
method: 'post',
43+
body: tokenBodyParams,
44+
headers: {
45+
'Content-Type': 'application/x-www-form-urlencoded',
46+
'authorization': 'Basic ' + Buffer.from('wrongId:wrongSecret').toString('base64'),
47+
}
48+
});
49+
50+
const body = await request({
51+
url: '/token',
52+
note: 'valid credentials',
53+
method: 'post',
54+
body: tokenBodyParams,
55+
headers: {
56+
'content-type': 'application/x-www-form-urlencoded',
57+
'authorization': 'Basic ' + Buffer.from(`${client.id}:${client.secret}`).toString('base64'),
58+
}
59+
});
60+
61+
62+
const token = JSON.parse(body);
63+
const accessToken = token.access_token;
64+
const tokenType = token.token_type;
65+
66+
if (accessToken && tokenType) {
67+
log('authorization token successfully retrieved!', '\n');
68+
}
69+
70+
await request({
71+
url: '/read-resource',
72+
note: 'authenticated, resource is not yet defined',
73+
headers: {
74+
'authorization': `${tokenType} ${accessToken}`
75+
}
76+
});
77+
78+
await request({
79+
url: '/write-resource',
80+
method: 'post',
81+
note: 'authentication failed',
82+
body: JSON.stringify({ value: 'foo-bar-moo' }),
83+
headers: {
84+
'content-type': 'application/json',
85+
'authorization': `${tokenType} random-token-foo`
86+
}
87+
});
88+
89+
await request({
90+
url: '/write-resource',
91+
method: 'post',
92+
note: 'Invalid token',
93+
body: JSON.stringify({ value: 'foo-bar-moo' }),
94+
headers: {
95+
'content-type': 'application/json',
96+
'authorization': `${tokenType} ${accessToken}`
97+
}
98+
});
99+
100+
101+
await request({
102+
url: '/read-resource',
103+
note: 'authenticated, resource is now',
104+
headers: {
105+
'authorization': `${tokenType} ${accessToken}`
106+
}
107+
});
108+
};
109+
110+
run().catch(console.error);

0 commit comments

Comments
 (0)