Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ DEFAULT_VC_POLICIES=expired,signature,revoked-status-list,not-before
ENABLE_LOGS=1
MODE_PROXY=1
MODE_PS=1
# Optional: if set, POST / requires X-API-Key to match this value
POLICY_SERVER_API_KEY=API_KEY_EXAMPLE
```

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

**Actions**

`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.

- `initiate`
- `getPD`
- `presentationRequest`
Expand Down Expand Up @@ -1416,6 +1420,8 @@ WALTID_ERROR_REDIRECT_URL=https://example.com/error?id=$id
ENABLE_LOGS=1
MODE_PROXY=1
MODE_PS=1
# Optional: if set, POST / requires X-API-Key to match this value
POLICY_SERVER_API_KEY=API_KEY_EXAMPLE
WALTID_VERIFY_RESPONSE_REDIRECT_URL=http://ocean-node-vm2.oceanenterprise.io:8100/verify/$id
WALTID_VERIFY_PRESENTATION_DEFINITION_URL=http://ocean-node-vm2.oceanenterprise.io:8100/pd/$id
DEFAULT_VP_POLICIES=expired,signature,revoked-status-list,not-before
Expand Down
108 changes: 64 additions & 44 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,73 @@ import {
} from './utils/verifyPresentationRequest.js'
import { downloadLogs } from './utils/logger.js'
import dotenv from 'dotenv'
import path from 'path'
import { fileURLToPath } from 'url'
import { policyServerApiKeyAuth } from './utils/auth.js'

dotenv.config()
const app = express()
const authType = process.env.AUTH_TYPE || 'waltid'
async function handlePolicyRequest(
req: Request<{}, {}, PolicyRequestPayload>,
res: Response
): Promise<void> {
const { action, ...rest } = req.body

const handler = PolicyHandlerFactory.createPolicyHandler(authType)
if (handler == null) {
res.status(404).json({
success: false,
status: 404,
message: `Handler for auth type "${authType}" is not found.`
})

export function createApp(): express.Express {
const app = express()
const authType = process.env.AUTH_TYPE || 'waltid'

async function handlePolicyRequest(
req: Request<{}, {}, PolicyRequestPayload>,
res: Response
): Promise<void> {
const { action, ...rest } = req.body

const handler = PolicyHandlerFactory.createPolicyHandler(authType)
if (handler == null) {
res.status(404).json({
success: false,
httpStatus: 404,
message: `Handler for auth type "${authType}" is not found.`
})
return
}

const payload: PolicyRequestPayload = { action, ...rest }
const response: PolicyRequestResponse = await handler.execute(payload)
res.status(response.httpStatus).json(response)
}

const payload: PolicyRequestPayload = { action, ...rest }
const response: PolicyRequestResponse = await handler.execute(payload)
res.status(response.httpStatus).json(response)
}
app.use(express.json())

app.use(express.json())
app.use(requestLogger)
if (process.env.MODE_PS && process.env.MODE_PS === '1') {
app.post('/', asyncHandler(handlePolicyRequest))
}
if (
process.env.OCEAN_NODE_URL &&
process.env.MODE_PROXY &&
process.env.MODE_PROXY === '1'
) {
app.post(
'/verify/:id',
express.urlencoded({ extended: true }),
asyncHandler(handleVerifyPresentationRequest)
)
app.get('/pd/:id', asyncHandler(handleGetPD))
}
if (process.env.ENABLE_LOGS && process.env.ENABLE_LOGS === '1') {
app.get('/logs', downloadLogs)
if (process.env.MODE_PS === '1') {
app.post(
'/',
policyServerApiKeyAuth,
requestLogger,
asyncHandler(handlePolicyRequest)
)
}
if (process.env.OCEAN_NODE_URL && process.env.MODE_PROXY === '1') {
app.post(
'/verify/:id',
express.urlencoded({ extended: true }),
requestLogger,
asyncHandler(handleVerifyPresentationRequest)
)
app.get('/pd/:id', requestLogger, asyncHandler(handleGetPD))
}
if (process.env.ENABLE_LOGS === '1') {
app.get('/logs', requestLogger, downloadLogs)
}
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc))
app.use(errorHandler)

return app
}
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc))
app.use(errorHandler)

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
export const app = createApp()

const currentModulePath = fileURLToPath(import.meta.url)
const entrypointPath = process.argv[1] ? path.resolve(process.argv[1]) : ''

if (entrypointPath === currentModulePath) {
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
}
3 changes: 2 additions & 1 deletion src/test/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ DEFAULT_VP_POLICIES=expired,signature,revoked-status-list,not-before
DEFAULT_VC_POLICIES=expired,signature,revoked-status-list,not-before
ENABLE_LOGS=1
MODE_PROXY=1
MODE_PS=1
MODE_PS=1
POLICY_SERVER_API_KEY=test-secret
8 changes: 8 additions & 0 deletions src/test/WaltIdPolicyHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
import { expect } from 'chai'
import sinon from 'sinon'
import axios from 'axios'
import dotenv from 'dotenv'
import path from 'path'
import { fileURLToPath } from 'url'
import { WaltIdPolicyHandler } from '../handlers/waltIdPolicyHandler.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

dotenv.config({ path: path.join(__dirname, '.env.test') })

describe('WaltIdPolicyHandler', () => {
let handler: WaltIdPolicyHandler

Expand Down
61 changes: 61 additions & 0 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { timingSafeEqual } from 'crypto'
import { Request, Response, NextFunction, RequestHandler } from 'express'
import { logWarn } from './logger.js'

const API_KEY_HEADER = 'x-api-key'
const REDACTED_VALUE = '[REDACTED]'
const SENSITIVE_HEADERS = new Set(['x-api-key', 'authorization', 'cookie', 'set-cookie'])

export function redactSensitiveHeaders(
headers: Request['headers']
): Record<string, string | string[] | undefined> {
return Object.entries(headers).reduce<Record<string, string | string[] | undefined>>(
(sanitizedHeaders, [key, value]) => {
sanitizedHeaders[key] = SENSITIVE_HEADERS.has(key.toLowerCase())
? REDACTED_VALUE
: value
return sanitizedHeaders
},
{}
)
}

function isApiKeyValid(providedApiKey: string, expectedApiKey: string): boolean {
const providedBuffer = Buffer.from(providedApiKey)
const expectedBuffer = Buffer.from(expectedApiKey)

if (providedBuffer.length !== expectedBuffer.length) return false

return timingSafeEqual(providedBuffer, expectedBuffer)
}

export const policyServerApiKeyAuth: RequestHandler = (
req: Request,
res: Response,
next: NextFunction
): void => {
const expectedApiKey = process.env.POLICY_SERVER_API_KEY?.trim()

if (!expectedApiKey) {
next()
return
}

const providedApiKey = req.get(API_KEY_HEADER)
if (!providedApiKey || !isApiKeyValid(providedApiKey, expectedApiKey)) {
logWarn({
method: req.method,
url: req.originalUrl,
headers: redactSensitiveHeaders(req.headers),
message: 'Policy Server API key authentication failed.'
})
res.status(401).json({
success: false,
message: 'Unauthorized',
httpStatus: 401
})
return
}

next()
}
5 changes: 3 additions & 2 deletions src/utils/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Request, Response, NextFunction, RequestHandler } from 'express'
import axios, { AxiosError } from 'axios'
import { PolicyRequestResponse } from '../@types/policy'
import { logError, logInfo } from './logger.js'
import { redactSensitiveHeaders } from './auth.js'

export const asyncHandler =
(fn: RequestHandler): RequestHandler =>
Expand All @@ -12,7 +13,7 @@ export const requestLogger = (req: Request, res: Response, next: NextFunction):
const logMessage = {
method: req.method,
url: req.originalUrl,
headers: req.headers,
headers: redactSensitiveHeaders(req.headers),
body: req.body
}

Expand All @@ -32,7 +33,7 @@ export const errorHandler = (
const logMessage = {
method: req.method,
url: req.originalUrl,
headers: req.headers,
headers: redactSensitiveHeaders(req.headers),
body: req.body,
error: {
message: err.message,
Expand Down
14 changes: 14 additions & 0 deletions swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
"/": {
"post": {
"summary": "Perform an action",
"parameters": [
{
"in": "header",
"name": "X-API-Key",
"required": false,
"schema": {
"type": "string"
},
"description": "Optional shared secret header. Required only when POLICY_SERVER_API_KEY is configured on the Policy Server."
}
],
"requestBody": {
"required": true,
"content": {
Expand Down Expand Up @@ -1218,6 +1229,9 @@
}
}
}
},
"401": {
"description": "Missing or invalid API key."
}
}
}
Expand Down
Loading