Skip to content

Commit

Permalink
Implement bearer token auth (#174)
Browse files Browse the repository at this point in the history
* Implement bearer token auth

* Readme configuration changes
  • Loading branch information
Siegrift authored Dec 31, 2023
1 parent 23b03ce commit a9f29df
Show file tree
Hide file tree
Showing 19 changed files with 233 additions and 81 deletions.
12 changes: 10 additions & 2 deletions packages/airnode-feed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,12 @@ Configuration for the signed APIs. Each signed API is defined by a `signedApiNam
example:

```jsonc
// Defines a single signed API.
// Defines a single signed API that uses AUTH_TOKEN secret as Bearer token when pushing signed data to signed API.
"signedApis": [
{
"name": "localhost",
"url": "http://localhost:8090"
"url": "http://localhost:8090",
"authToken": "${AUTH_TOKEN}"
}
]
```
Expand All @@ -304,6 +305,13 @@ The name of the signed API.

The URL of the signed API.

#### `authToken`

The authentication token used to authenticate with the signed API. It is recommended to interpolate this value from
secrets.

If the signed API does not require authentication, set this value to `null`.

#### `ois`

Configuration for the OISes.
Expand Down
5 changes: 3 additions & 2 deletions packages/airnode-feed/config/airnode-feed.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@
"0x1d65c1f1e127a41cebd2339f823d0290322c63f3044380cbac105db8e522ebb9"
],
"fetchInterval": 5,
"updateDelay": 30
"updateDelay": 0
}
]
},
"signedApis": [
{
"name": "localhost",
"url": "http://localhost:8090"
"url": "http://localhost:8090",
"authToken": "some-secret-token-for-airnode-feed"
}
],
"ois": [
Expand Down
5 changes: 3 additions & 2 deletions packages/airnode-feed/src/api-requests/signed-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ export const pushSignedData = async (group: SignedApiUpdate) => {
}

logger.debug('Posting signed API data.', { group });
const provider = signedApis.find((a) => a.name === signedApiName)!;
const signedApi = signedApis.find((a) => a.name === signedApiName)!;
const goAxiosRequest = await go<Promise<unknown>, AxiosError>(async () => {
logger.debug('Posting batch payload.', { batchPayload });
const axiosResponse = await axios.post(provider.url, batchPayload, {
const axiosResponse = await axios.post(signedApi.url, batchPayload, {
headers: {
'Content-Type': 'application/json',
...(signedApi.authToken ? { Authorization: `Bearer ${signedApi.authToken}` } : {}),
},
});

Expand Down
12 changes: 6 additions & 6 deletions packages/airnode-feed/src/heartbeat/heartbeat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ describe(logHeartbeat.name, () => {
nodeVersion: '0.1.0',
currentTimestamp: '1674172803',
deploymentTimestamp: '1674172800',
configHash: '0x1a2a00f22eeab37eb95f8cf1ec10ec89521516db42f41b7ffc6da541d948a54a',
configHash: '0x0a36630da26fa987561ff8b692f2015a6fe632bdabcf3dcdd010ccc8262f4a3a',
signature:
'0xd0f84a62705585e03c14ebfda2688a4e4c733aa42a3d13f98d4fe390d482fd1c3e698e91939df28b9565a9de82f3495b053824ffe61239fdd4fa0e4a970523a81b',
'0x15fb32178d3c6e30385e448b21a4b9086c715a11e8044513bf3b6a578643f7a327498b59cc3d9442fbd2f3b3b4991f94398727e54558ac24871e2df44d1664e11c',
};
const rawConfig = JSON.parse(readFileSync(join(__dirname, '../../config/airnode-feed.example.json'), 'utf8'));
jest.spyOn(configModule, 'loadRawConfig').mockReturnValue(rawConfig);
Expand All @@ -50,12 +50,12 @@ describe(verifyHeartbeatLog.name, () => {
const jsonLog = {
context: {
airnode: '0xbF3137b0a7574563a23a8fC8badC6537F98197CC',
configHash: '0x1a2a00f22eeab37eb95f8cf1ec10ec89521516db42f41b7ffc6da541d948a54a',
configHash: '0x0a36630da26fa987561ff8b692f2015a6fe632bdabcf3dcdd010ccc8262f4a3a',
currentTimestamp: '1674172803',
deploymentTimestamp: '1674172800',
nodeVersion: '0.1.0',
signature:
'0xd0f84a62705585e03c14ebfda2688a4e4c733aa42a3d13f98d4fe390d482fd1c3e698e91939df28b9565a9de82f3495b053824ffe61239fdd4fa0e4a970523a81b',
'0x15fb32178d3c6e30385e448b21a4b9086c715a11e8044513bf3b6a578643f7a327498b59cc3d9442fbd2f3b3b4991f94398727e54558ac24871e2df44d1664e11c',
stage: 'test',
},
level: 'info',
Expand All @@ -81,10 +81,10 @@ describe(stringifyUnsignedHeartbeatPayload.name, () => {
nodeVersion: '0.1.0',
currentTimestamp: '1674172803',
deploymentTimestamp: '1674172800',
configHash: '0x1a2a00f22eeab37eb95f8cf1ec10ec89521516db42f41b7ffc6da541d948a54a',
configHash: '0x0a36630da26fa987561ff8b692f2015a6fe632bdabcf3dcdd010ccc8262f4a3a',
})
).toBe(
'{"airnode":"0xbF3137b0a7574563a23a8fC8badC6537F98197CC","configHash":"0x1a2a00f22eeab37eb95f8cf1ec10ec89521516db42f41b7ffc6da541d948a54a","currentTimestamp":"1674172803","deploymentTimestamp":"1674172800","nodeVersion":"0.1.0","stage":"test"}'
'{"airnode":"0xbF3137b0a7574563a23a8fC8badC6537F98197CC","configHash":"0x0a36630da26fa987561ff8b692f2015a6fe632bdabcf3dcdd010ccc8262f4a3a","currentTimestamp":"1674172803","deploymentTimestamp":"1674172800","nodeVersion":"0.1.0","stage":"test"}'
);
});
});
Expand Down
7 changes: 4 additions & 3 deletions packages/airnode-feed/src/validation/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ test('ensures nodeVersion matches Airnode feed version', async () => {
test('ensures signed API names are unique', () => {
expect(() =>
signedApisSchema.parse([
{ name: 'foo', url: 'https://example.com' },
{ name: 'foo', url: 'https://example.com' },
{ name: 'foo', url: 'https://example.com', authToken: null },
{ name: 'foo', url: 'https://example.com', authToken: null },
])
).toThrow(
new ZodError([
Expand All @@ -65,10 +65,11 @@ test('ensures signed API names are unique', () => {
])
);

expect(signedApisSchema.parse([{ name: 'foo', url: 'https://example.com' }])).toStrictEqual([
expect(signedApisSchema.parse([{ name: 'foo', url: 'https://example.com', authToken: null }])).toStrictEqual([
{
name: 'foo',
url: 'https://example.com',
authToken: null,
},
]);
});
Expand Down
1 change: 1 addition & 0 deletions packages/airnode-feed/src/validation/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ const validateTriggerReferences: SuperRefinement<{
export const signedApiSchema = z.strictObject({
name: z.string(),
url: z.string().url(),
authToken: z.string().nullable(),
});

export type SignedApi = z.infer<typeof signedApiSchema>;
Expand Down
1 change: 1 addition & 0 deletions packages/airnode-feed/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const config: Config = {
{
name: 'localhost',
url: 'http://localhost:8090',
authToken: null,
},
],
ois: [
Expand Down
49 changes: 41 additions & 8 deletions packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,17 @@ The API needs to be configured with endpoints to be served. This is done via the
```jsonc
// Defines two endpoints.
"endpoints": [
// Serves the non-delayed data on URL path "/real-time".
// Serves the non-delayed data on URL path "/real-time". Requesters need to provide the "some-secret-token" as Bearer token.
{
"urlPath": "/real-time",
"delaySeconds": 0
"delaySeconds": 0,
"authTokens": ["some-secret-token"],
},
// Serves the data delayed by 15 seconds on URL path "/delayed".
// Serves the data delayed by 15 seconds on URL path "/delayed". No authentication is required.
{
"urlPath": "/delayed",
"delaySeconds": 15
"delaySeconds": 15,
"authTokens": null,
}
]
```
Expand All @@ -171,6 +173,14 @@ dashes.

The delay in seconds for the endpoint. The endpoint will only serve data that is older than the delay.

###### `authTokens`

The nonempty list of
[Bearer authentication tokens](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#bearer) allowed to query
the data.

In case the endpoint should be publicly available, set the value to `null`.

#### `cache` _(optional)_

Configures the cache for the API endpoints.
Expand All @@ -190,8 +200,8 @@ The maximum age of the cache in seconds. The cache is cleared after this time.

#### `allowedAirnodes`

The list of allowed Airnode addresses. If the list is empty, no Airnode is allowed. To whitelist all Airnodes, set the
value to `"*"` instead of an array.
The list of allowed Airnodes with authorization details. If the list is empty, no Airnode is allowed. To whitelist all
Airnodes, set the value to `"*"` instead of an array.

Example:

Expand All @@ -203,10 +213,33 @@ Example:
or

```jsonc
// Allows pushing signed data only from the specific Airnode.
"allowedAirnodes": ["0xB47E3D8734780430ee6EfeF3c5407090601Dcd15"]
// Allows pushing signed data only for the specific Airnode. No authorization is required to push the data.
"allowedAirnodes": [ { "address": "0xB47E3D8734780430ee6EfeF3c5407090601Dcd15", "authTokens": null } ]
```

or

```jsonc
// Allows pushing signed data only for the specific Airnode. The pusher needs to authorize with one of the specific tokens.
"allowedAirnodes": { "address": "0xbF3137b0a7574563a23a8fC8badC6537F98197CC", "authTokens": ["some-secret-token-for-airnode-feed"] }
```

##### `allowedAirnodes[n]`

One of the allowed Airnodes.

###### `address`

The address of the Airnode. The address must be a valid Ethereum address.

###### `authTokens`

The nonempty list of
[Bearer authentication tokens](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#bearer).

To allow pushing data without any authorization, set the value to `null`. The API validates the data, but this is not
recommended.

##### `stage`

An identifier of the deployment stage. This is used to distinguish between different deployments of Signed API, for
Expand Down
6 changes: 5 additions & 1 deletion packages/api/config/signed-api.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
"endpoints": [
{
"urlPath": "/real-time",
"authTokens": ["some-secret-token"],
"delaySeconds": 0
},
{
"urlPath": "/delayed",
"authTokens": null,
"delaySeconds": 15
}
],
"allowedAirnodes": ["0xbF3137b0a7574563a23a8fC8badC6537F98197CC"],
"allowedAirnodes": [
{ "address": "0xbF3137b0a7574563a23a8fC8badC6537F98197CC", "authTokens": ["some-secret-token-for-airnode-feed"] }
],
"stage": "local",
"version": "0.1.0"
}
Loading

0 comments on commit a9f29df

Please sign in to comment.