Skip to content

Commit 6f0d7b9

Browse files
Merge pull request #24 from OceanProtocolEnterprise/feat/apy-key
chore: added api key
2 parents 2bd7625 + 08b98ac commit 6f0d7b9

File tree

7 files changed

+158
-47
lines changed

7 files changed

+158
-47
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ DEFAULT_VC_POLICIES=expired,signature,revoked-status-list,not-before
2121
ENABLE_LOGS=1
2222
MODE_PROXY=1
2323
MODE_PS=1
24+
# Optional: if set, POST / requires X-API-Key to match this value
25+
POLICY_SERVER_API_KEY=API_KEY_EXAMPLE
2426
```
2527

2628
1. Start the Docker container:
@@ -36,6 +38,8 @@ MODE_PS=1
3638

3739
**Actions**
3840

41+
`POLICY_SERVER_API_KEY` is optional. If it is configured, requests to `POST /` must include `X-API-Key: <POLICY_SERVER_API_KEY>`, and missing or invalid keys are rejected with `401 Unauthorized` before any action is processed. If it is not configured, the Policy Server accepts requests without API key authentication.
42+
3943
- `initiate`
4044
- `getPD`
4145
- `presentationRequest`
@@ -1416,6 +1420,8 @@ WALTID_ERROR_REDIRECT_URL=https://example.com/error?id=$id
14161420
ENABLE_LOGS=1
14171421
MODE_PROXY=1
14181422
MODE_PS=1
1423+
# Optional: if set, POST / requires X-API-Key to match this value
1424+
POLICY_SERVER_API_KEY=API_KEY_EXAMPLE
14191425
WALTID_VERIFY_RESPONSE_REDIRECT_URL=http://ocean-node-vm2.oceanenterprise.io:8100/verify/$id
14201426
WALTID_VERIFY_PRESENTATION_DEFINITION_URL=http://ocean-node-vm2.oceanenterprise.io:8100/pd/$id
14211427
DEFAULT_VP_POLICIES=expired,signature,revoked-status-list,not-before

src/index.ts

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,73 @@ import {
1010
} from './utils/verifyPresentationRequest.js'
1111
import { downloadLogs } from './utils/logger.js'
1212
import dotenv from 'dotenv'
13+
import path from 'path'
14+
import { fileURLToPath } from 'url'
15+
import { policyServerApiKeyAuth } from './utils/auth.js'
16+
1317
dotenv.config()
14-
const app = express()
15-
const authType = process.env.AUTH_TYPE || 'waltid'
16-
async function handlePolicyRequest(
17-
req: Request<{}, {}, PolicyRequestPayload>,
18-
res: Response
19-
): Promise<void> {
20-
const { action, ...rest } = req.body
21-
22-
const handler = PolicyHandlerFactory.createPolicyHandler(authType)
23-
if (handler == null) {
24-
res.status(404).json({
25-
success: false,
26-
status: 404,
27-
message: `Handler for auth type "${authType}" is not found.`
28-
})
18+
19+
export function createApp(): express.Express {
20+
const app = express()
21+
const authType = process.env.AUTH_TYPE || 'waltid'
22+
23+
async function handlePolicyRequest(
24+
req: Request<{}, {}, PolicyRequestPayload>,
25+
res: Response
26+
): Promise<void> {
27+
const { action, ...rest } = req.body
28+
29+
const handler = PolicyHandlerFactory.createPolicyHandler(authType)
30+
if (handler == null) {
31+
res.status(404).json({
32+
success: false,
33+
httpStatus: 404,
34+
message: `Handler for auth type "${authType}" is not found.`
35+
})
36+
return
37+
}
38+
39+
const payload: PolicyRequestPayload = { action, ...rest }
40+
const response: PolicyRequestResponse = await handler.execute(payload)
41+
res.status(response.httpStatus).json(response)
2942
}
3043

31-
const payload: PolicyRequestPayload = { action, ...rest }
32-
const response: PolicyRequestResponse = await handler.execute(payload)
33-
res.status(response.httpStatus).json(response)
34-
}
44+
app.use(express.json())
3545

36-
app.use(express.json())
37-
app.use(requestLogger)
38-
if (process.env.MODE_PS && process.env.MODE_PS === '1') {
39-
app.post('/', asyncHandler(handlePolicyRequest))
40-
}
41-
if (
42-
process.env.OCEAN_NODE_URL &&
43-
process.env.MODE_PROXY &&
44-
process.env.MODE_PROXY === '1'
45-
) {
46-
app.post(
47-
'/verify/:id',
48-
express.urlencoded({ extended: true }),
49-
asyncHandler(handleVerifyPresentationRequest)
50-
)
51-
app.get('/pd/:id', asyncHandler(handleGetPD))
52-
}
53-
if (process.env.ENABLE_LOGS && process.env.ENABLE_LOGS === '1') {
54-
app.get('/logs', downloadLogs)
46+
if (process.env.MODE_PS === '1') {
47+
app.post(
48+
'/',
49+
policyServerApiKeyAuth,
50+
requestLogger,
51+
asyncHandler(handlePolicyRequest)
52+
)
53+
}
54+
if (process.env.OCEAN_NODE_URL && process.env.MODE_PROXY === '1') {
55+
app.post(
56+
'/verify/:id',
57+
express.urlencoded({ extended: true }),
58+
requestLogger,
59+
asyncHandler(handleVerifyPresentationRequest)
60+
)
61+
app.get('/pd/:id', requestLogger, asyncHandler(handleGetPD))
62+
}
63+
if (process.env.ENABLE_LOGS === '1') {
64+
app.get('/logs', requestLogger, downloadLogs)
65+
}
66+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc))
67+
app.use(errorHandler)
68+
69+
return app
5570
}
56-
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc))
57-
app.use(errorHandler)
5871

59-
const PORT = process.env.PORT || 3000
60-
app.listen(PORT, () => {
61-
console.log(`Server is running on port ${PORT}`)
62-
})
72+
export const app = createApp()
73+
74+
const currentModulePath = fileURLToPath(import.meta.url)
75+
const entrypointPath = process.argv[1] ? path.resolve(process.argv[1]) : ''
76+
77+
if (entrypointPath === currentModulePath) {
78+
const PORT = process.env.PORT || 3000
79+
app.listen(PORT, () => {
80+
console.log(`Server is running on port ${PORT}`)
81+
})
82+
}

src/test/.env.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ DEFAULT_VP_POLICIES=expired,signature,revoked-status-list,not-before
99
DEFAULT_VC_POLICIES=expired,signature,revoked-status-list,not-before
1010
ENABLE_LOGS=1
1111
MODE_PROXY=1
12-
MODE_PS=1
12+
MODE_PS=1
13+
POLICY_SERVER_API_KEY=test-secret

src/test/WaltIdPolicyHandler.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
import { expect } from 'chai'
33
import sinon from 'sinon'
44
import axios from 'axios'
5+
import dotenv from 'dotenv'
6+
import path from 'path'
7+
import { fileURLToPath } from 'url'
58
import { WaltIdPolicyHandler } from '../handlers/waltIdPolicyHandler.js'
69

10+
const __filename = fileURLToPath(import.meta.url)
11+
const __dirname = path.dirname(__filename)
12+
13+
dotenv.config({ path: path.join(__dirname, '.env.test') })
14+
715
describe('WaltIdPolicyHandler', () => {
816
let handler: WaltIdPolicyHandler
917

src/utils/auth.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { timingSafeEqual } from 'crypto'
2+
import { Request, Response, NextFunction, RequestHandler } from 'express'
3+
import { logWarn } from './logger.js'
4+
5+
const API_KEY_HEADER = 'x-api-key'
6+
const REDACTED_VALUE = '[REDACTED]'
7+
const SENSITIVE_HEADERS = new Set(['x-api-key', 'authorization', 'cookie', 'set-cookie'])
8+
9+
export function redactSensitiveHeaders(
10+
headers: Request['headers']
11+
): Record<string, string | string[] | undefined> {
12+
return Object.entries(headers).reduce<Record<string, string | string[] | undefined>>(
13+
(sanitizedHeaders, [key, value]) => {
14+
sanitizedHeaders[key] = SENSITIVE_HEADERS.has(key.toLowerCase())
15+
? REDACTED_VALUE
16+
: value
17+
return sanitizedHeaders
18+
},
19+
{}
20+
)
21+
}
22+
23+
function isApiKeyValid(providedApiKey: string, expectedApiKey: string): boolean {
24+
const providedBuffer = Buffer.from(providedApiKey)
25+
const expectedBuffer = Buffer.from(expectedApiKey)
26+
27+
if (providedBuffer.length !== expectedBuffer.length) return false
28+
29+
return timingSafeEqual(providedBuffer, expectedBuffer)
30+
}
31+
32+
export const policyServerApiKeyAuth: RequestHandler = (
33+
req: Request,
34+
res: Response,
35+
next: NextFunction
36+
): void => {
37+
const expectedApiKey = process.env.POLICY_SERVER_API_KEY?.trim()
38+
39+
if (!expectedApiKey) {
40+
next()
41+
return
42+
}
43+
44+
const providedApiKey = req.get(API_KEY_HEADER)
45+
if (!providedApiKey || !isApiKeyValid(providedApiKey, expectedApiKey)) {
46+
logWarn({
47+
method: req.method,
48+
url: req.originalUrl,
49+
headers: redactSensitiveHeaders(req.headers),
50+
message: 'Policy Server API key authentication failed.'
51+
})
52+
res.status(401).json({
53+
success: false,
54+
message: 'Unauthorized',
55+
httpStatus: 401
56+
})
57+
return
58+
}
59+
60+
next()
61+
}

src/utils/middleware.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Request, Response, NextFunction, RequestHandler } from 'express'
22
import axios, { AxiosError } from 'axios'
33
import { PolicyRequestResponse } from '../@types/policy'
44
import { logError, logInfo } from './logger.js'
5+
import { redactSensitiveHeaders } from './auth.js'
56

67
export const asyncHandler =
78
(fn: RequestHandler): RequestHandler =>
@@ -12,7 +13,7 @@ export const requestLogger = (req: Request, res: Response, next: NextFunction):
1213
const logMessage = {
1314
method: req.method,
1415
url: req.originalUrl,
15-
headers: req.headers,
16+
headers: redactSensitiveHeaders(req.headers),
1617
body: req.body
1718
}
1819

@@ -32,7 +33,7 @@ export const errorHandler = (
3233
const logMessage = {
3334
method: req.method,
3435
url: req.originalUrl,
35-
headers: req.headers,
36+
headers: redactSensitiveHeaders(req.headers),
3637
body: req.body,
3738
error: {
3839
message: err.message,

swagger.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
"/": {
1010
"post": {
1111
"summary": "Perform an action",
12+
"parameters": [
13+
{
14+
"in": "header",
15+
"name": "X-API-Key",
16+
"required": false,
17+
"schema": {
18+
"type": "string"
19+
},
20+
"description": "Optional shared secret header. Required only when POLICY_SERVER_API_KEY is configured on the Policy Server."
21+
}
22+
],
1223
"requestBody": {
1324
"required": true,
1425
"content": {
@@ -1218,6 +1229,9 @@
12181229
}
12191230
}
12201231
}
1232+
},
1233+
"401": {
1234+
"description": "Missing or invalid API key."
12211235
}
12221236
}
12231237
}

0 commit comments

Comments
 (0)