Skip to content

Commit

Permalink
Extensions Endpoints Guides Security Improvements (#19547)
Browse files Browse the repository at this point in the history
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
Co-authored-by: Brainslug <br41nslug@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 4, 2023
1 parent 1c8ab87 commit da84c67
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 124 deletions.
8 changes: 6 additions & 2 deletions docs/.vitepress/data/guides.data.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,15 @@ export default {
title: 'Endpoints',
items: [
{
display: 'Use Custom Endpoints to Create an API Proxy',
display: 'Create a Public API Proxy',
path: '/guides/extensions/endpoints-api-proxy',
},
{
display: 'Create an Authenticated API Proxy (Twilio)',
path: '/guides/extensions/endpoints-api-proxy-twilio',
},
{
display: 'Use Custom Endpoints to Create Privileged API Endpoints',
display: 'Create a Permissions-Based API Proxy (Stripe)',
path: '/guides/extensions/endpoints-privileged-endpoint-stripe',
},
],
Expand Down
2 changes: 2 additions & 0 deletions docs/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ Pino
Plesk
PNG
pnpm
PokéAPI
Pokémon
Postgres
Postgres13
PR
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/extensions/displays-date-to-age.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: 'Learn how to create a display extension to format an item value.'
description: Learn how to create a display extension to format an item value.
contributors: Tim Butterfield, Kevin Lewis
---

Expand Down
223 changes: 166 additions & 57 deletions docs/guides/extensions/endpoints-api-proxy-twilio.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
description: 'Learn how to proxy a third-party API with a custom endpoint.'
description: Learn how to proxy a third-party API with a custom endpoint, requiring a valid user account.
contributors: Tim Butterfield, Kevin Lewis
---

# Use Custom Endpoints to Create an API Proxy
# Use Custom Endpoints to Create an Authenticated API Proxy

Endpoints are used in the API to perform certain functions.

Expand Down Expand Up @@ -50,26 +50,17 @@ variables, add the `env` context to the handler like so:
handler: (router, { env }) => {
```
To perform the API query, add the `axios` package which is included in Directus.
Create variables for Twilio and construct the request headers object for Basic Authentication:
```js
const axios = require('axios');
```
Create variables for the Twilio auth details, and then create a new `axios` instance with them:
const twilioHost = 'https://api.twilio.com';
const twilioSid = env.TWILIO_ACCOUNT_SID;
const twilioToken = env.TWILIO_AUTH_TOKEN;

```js
const twilio_host = "https://api.twilio.com";
const twilio_sid = env.TWILIO_ACCOUNT_SID;
const twilio_token = env.TWILIO_AUTH_TOKEN;

const twilio_api = axios.create({
baseURL: twilio_host,
auth: {
username: twilio_sid,
password: twilio_token,
},
});
const token = Buffer.from(`${twilioSid}:${twilioToken}`).toString('base64');
const headers = {
Authorization: `Basic ${token}`,
};
```
_Note: the client initialization values are unique to Twilio. Other 3rd Party services may authentication differently,
Expand All @@ -79,28 +70,113 @@ The standard way to create an API route is to specify the method and the path. R
endpoint that Twilio has, use a wildcard (\*) to run this function for every route for each supported method.
```js
router.get('/*', (req, res) => {
twilio_api.get(req.url).then((response) => {
res.json(response.data);
}).catch((error) => {
res.send(error);
});
router.get('/*', async (req, res) => {
try {
const response = await fetch(new URL(req.url, twilioHost), { headers });

if (response.ok) {
res.json(await response.json());
} else {
res.status(response.status);
res.send(response.statusText);
}
} catch (error) {
res.status(500);
res.send(error.message);
}
});

router.post('/*', (req, res) => {
twilio_api.post(req.url, new URLSearchParams(req.body)).then((response) => {
res.json(response.data);
}).catch((error) => {
res.send(error);
});
});
router.post('/*', async (req, res) => {
try {
const response = await fetch(new URL(req.url, twilioHost), {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});

if (response.ok) {
res.json(await response.json());
} else {
res.status(response.status);
res.send(response.statusText);
}
} catch (error) {
res.status(500);
res.send(error.message);
}
});
```
Each route includes the request (`req`) and response (`res`). The request has useful information that was provided by
the user or application such as the URL, method, authentication and other HTTP headers. In this case, the URL needs to
be combined with the twilio host to perform an API query.
### Ensure User Is Authenticated
As Twilio is an API that requires authentication and costs money to use, you should also require authentication for your
endpoint. Without this, any person on the internet could use it.
At the top of your file, import the `createError` function and create a new error:
```js
import { createError } from '@directus/errors';
const ForbiddenError = createError('TWILIO_FORBIDDEN', 'You need to be authenticated to access this endpoint');
```
Throw the function if `req.accountability` is `null`:
```js
router.get('/*', async (req, res) => {
if(req.accountability == null) { // [!code ++]
throw new ForbiddenError(); // [!code ++]
} // [!code ++]

try {
const response = await fetch(new URL(req.url, twilioHost), { headers });

if (response.ok) {
res.json(await response.json());
} else {
res.status(response.status);
res.send(response.statusText);
}
} catch (error) {
res.status(500);
res.send(error.message);
}
});

router.post('/*', async (req, res) => {
if(req.accountability == null) { // [!code ++]
throw new ForbiddenError(); // [!code ++]
} // [!code ++]

try {
const response = await fetch(new URL(req.url, twilioHost), {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});

if (response.ok) {
res.json(await response.json());
} else {
res.status(response.status);
res.send(response.statusText);
}
} catch (error) {
res.status(500);
res.send(error.message);
}
});
```
This is now complete and ready for testing. Build the endpoint with the latest changes.
```
Expand Down Expand Up @@ -168,36 +244,69 @@ simplify your other extensions.
`index.js`
```js
import { createError } from '@directus/errors';
const ForbiddenError = createError(
'TWILIO_FORBIDDEN',
'You need to be authenticated to access this endpoint'
);
export default {
id: 'twilio',
handler: (router, { env }) => {
const axios = require('axios');
const twilio_host = "https://api.twilio.com";
const twilio_sid = env.TWILIO_ACCOUNT_SID;
const twilio_token = env.TWILIO_AUTH_TOKEN;
const twilio_from = env.TWILIO_PHONE_NUMBER;
const twilio_api = axios.create({
baseURL: twilio_host,
auth: {
username: twilio_sid,
password: twilio_token,
},
});
router.get('/*', (req, res) => {
twilio_api.get(req.url).then((response) => {
res.json(response.data);
}).catch((error) => {
res.send(error);
});
const twilioHost = 'https://api.twilio.com';
const twilioSid = env.TWILIO_ACCOUNT_SID;
const twilioToken = env.TWILIO_AUTH_TOKEN;

const token = Buffer.from(`${twilioSid}:${twilioToken}`).toString('base64');
const headers = {
Authorization: `Basic ${token}`,
};

router.get('/*', async (req, res) => {
if (req.accountability == null) {
throw new ForbiddenError();
}

try {
const response = await fetch(new URL(req.url, twilioHost), { headers });

if (response.ok) {
res.json(await response.json());
} else {
res.status(response.status);
res.send(response.statusText);
}
} catch (error) {
res.status(500);
res.send(error.message);
}
});

router.post('/*', (req, res) => {
twilio_api.post(req.url, new URLSearchParams(req.body)).then((response) => {
res.json(response.data);
}).catch((error) => {
res.send(error);
});
router.post('/*', async (req, res) => {
if (req.accountability == null) {
throw new ForbiddenError();
}

try {
const response = await fetch(new URL(req.url, twilioHost), {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});

if (response.ok) {
res.json(await response.json());
} else {
res.status(response.status);
res.send(response.statusText);
}
} catch (error) {
res.status(500);
res.send(error.message);
}
});
},
};
Expand Down

0 comments on commit da84c67

Please sign in to comment.