diff --git a/packages/cactus-cmd-api-server/package.json b/packages/cactus-cmd-api-server/package.json
index bc904396d8..964dc244f3 100644
--- a/packages/cactus-cmd-api-server/package.json
+++ b/packages/cactus-cmd-api-server/package.json
@@ -89,6 +89,7 @@
"fastify": "4.28.1",
"fs-extra": "11.2.0",
"google-protobuf": "3.21.4",
+ "http-status-codes": "2.3.0",
"jose": "4.15.5",
"json-stable-stringify": "1.0.2",
"lmify": "0.3.0",
diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md
index 548fe6c56c..00c6d62c51 100644
--- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md
+++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md
@@ -91,7 +91,18 @@ Class | Method | HTTP request | Description
## Documentation For Authorization
-Endpoints do not require authorization.
+
+Authentication schemes defined for the API:
+### bearerTokenAuth
+
+- **Type**: HTTP Bearer token authentication
+
+Example
+
+```golang
+auth := context.WithValue(context.Background(), sw.ContextAccessToken, "BEARER_TOKEN_STRING")
+r, err := client.Service.Operation(auth, args)
+```
## Documentation for Utility Methods
diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml
index b25ac93cc3..892145d84a 100644
--- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml
+++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml
@@ -8,6 +8,11 @@ info:
version: 2.0.0
servers:
- url: /
+security:
+- bearerTokenAuth:
+ - read:health
+ - read:metrics
+ - read:spec
paths:
/api/v1/api-server/healthcheck:
get:
@@ -21,6 +26,13 @@ paths:
schema:
$ref: '#/components/schemas/HealthCheckResponse'
description: OK
+ "401":
+ description: Unauthorized
+ "403":
+ description: Valid token but missing correct scope
+ security:
+ - bearerTokenAuth:
+ - read:health
summary: Can be used to verify liveness of an API server instance
x-hyperledger-cacti:
http:
@@ -37,6 +49,13 @@ paths:
schema:
$ref: '#/components/schemas/PrometheusExporterMetricsResponse'
description: OK
+ "401":
+ description: Unauthorized
+ "403":
+ description: Valid token but missing correct scope
+ security:
+ - bearerTokenAuth:
+ - read:metrics
summary: Get the Prometheus Metrics
x-hyperledger-cacti:
http:
@@ -54,6 +73,13 @@ paths:
schema:
$ref: '#/components/schemas/GetOpenApiSpecV1EndpointResponse'
description: OK
+ "401":
+ description: Unauthorized
+ "403":
+ description: Valid token but missing correct scope
+ security:
+ - bearerTokenAuth:
+ - read:spec
x-hyperledger-cacti:
http:
verbLowerCase: get
@@ -127,3 +153,8 @@ components:
GetOpenApiSpecV1EndpointResponse:
nullable: false
type: string
+ securitySchemes:
+ bearerTokenAuth:
+ bearerFormat: JSON Web Tokens
+ scheme: bearer
+ type: http
diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go
index a3b0f19744..b9e16de3b6 100644
--- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go
+++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go
@@ -410,6 +410,11 @@ func (c *APIClient) prepareRequest(
// Walk through any authentication.
+ // AccessToken Authentication
+ if auth, ok := ctx.Value(ContextAccessToken).(string); ok {
+ localVarRequest.Header.Add("Authorization", "Bearer "+auth)
+ }
+
}
for header, value := range c.cfg.DefaultHeader {
diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go
index c69a1313a5..6242dfec4f 100644
--- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go
+++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go
@@ -28,6 +28,9 @@ func (c contextKey) String() string {
}
var (
+ // ContextAccessToken takes a string oauth2 access token as authentication for the request.
+ ContextAccessToken = contextKey("accesstoken")
+
// ContextServerIndex uses a server configuration from the index.
ContextServerIndex = contextKey("serverIndex")
diff --git a/packages/cactus-cmd-api-server/src/main/json/openapi.json b/packages/cactus-cmd-api-server/src/main/json/openapi.json
index a9c63cf9d2..3be247a00b 100644
--- a/packages/cactus-cmd-api-server/src/main/json/openapi.json
+++ b/packages/cactus-cmd-api-server/src/main/json/openapi.json
@@ -76,8 +76,20 @@
"type": "string",
"nullable": false
}
+ },
+ "securitySchemes": {
+ "bearerTokenAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JSON Web Tokens"
+ }
}
},
+ "security": [
+ {
+ "bearerTokenAuth": ["read:health", "read:metrics", "read:spec"]
+ }
+ ],
"paths": {
"/api/v1/api-server/healthcheck": {
"get": {
@@ -101,8 +113,19 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Valid token but missing correct scope"
}
- }
+ },
+ "security": [
+ {
+ "bearerTokenAuth": ["read:health"]
+ }
+ ]
}
},
"/api/v1/api-server/get-prometheus-exporter-metrics": {
@@ -126,8 +149,19 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Valid token but missing correct scope"
}
- }
+ },
+ "security": [
+ {
+ "bearerTokenAuth": ["read:metrics"]
+ }
+ ]
}
},
"/api/v1/api-server/get-open-api-spec": {
@@ -151,8 +185,19 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Valid token but missing correct scope"
}
- }
+ },
+ "security": [
+ {
+ "bearerTokenAuth": ["read:spec"]
+ }
+ ]
}
}
}
diff --git a/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json b/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json
index a9c63cf9d2..3be247a00b 100644
--- a/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json
+++ b/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json
@@ -76,8 +76,20 @@
"type": "string",
"nullable": false
}
+ },
+ "securitySchemes": {
+ "bearerTokenAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JSON Web Tokens"
+ }
}
},
+ "security": [
+ {
+ "bearerTokenAuth": ["read:health", "read:metrics", "read:spec"]
+ }
+ ],
"paths": {
"/api/v1/api-server/healthcheck": {
"get": {
@@ -101,8 +113,19 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Valid token but missing correct scope"
}
- }
+ },
+ "security": [
+ {
+ "bearerTokenAuth": ["read:health"]
+ }
+ ]
}
},
"/api/v1/api-server/get-prometheus-exporter-metrics": {
@@ -126,8 +149,19 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Valid token but missing correct scope"
}
- }
+ },
+ "security": [
+ {
+ "bearerTokenAuth": ["read:metrics"]
+ }
+ ]
}
},
"/api/v1/api-server/get-open-api-spec": {
@@ -151,8 +185,19 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Valid token but missing correct scope"
}
- }
+ },
+ "security": [
+ {
+ "bearerTokenAuth": ["read:spec"]
+ }
+ ]
}
}
}
diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md
index a9770d9c2e..3159e738ac 100644
--- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md
+++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md
@@ -60,5 +60,10 @@ Class | Method | HTTP request | Description
## Documentation for Authorization
-Endpoints do not require authorization.
+
+Authentication schemes defined for the API:
+
+### bearerTokenAuth
+
+- **Type**: HTTP basic authentication
diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt
index d05dc1394b..3631592222 100644
--- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt
+++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt
@@ -108,7 +108,7 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient
path = "/api/v1/api-server/healthcheck",
query = localVariableQuery,
headers = localVariableHeaders,
- requiresAuthentication = false,
+ requiresAuthentication = true,
body = localVariableBody
)
}
@@ -176,7 +176,7 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient
path = "/api/v1/api-server/get-open-api-spec",
query = localVariableQuery,
headers = localVariableHeaders,
- requiresAuthentication = false,
+ requiresAuthentication = true,
body = localVariableBody
)
}
@@ -243,7 +243,7 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient
path = "/api/v1/api-server/get-prometheus-exporter-metrics",
query = localVariableQuery,
headers = localVariableHeaders,
- requiresAuthentication = false,
+ requiresAuthentication = true,
body = localVariableBody
)
}
diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt
index ea4b7b6593..c83d4d4d57 100644
--- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt
+++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt
@@ -143,10 +143,20 @@ open class ApiClient(val baseUrl: String, val client: OkHttpClient = defaultClie
}
}
+ protected fun updateAuthParams(requestConfig: RequestConfig) {
+ if (requestConfig.headers[Authorization].isNullOrEmpty()) {
+ accessToken?.let { accessToken ->
+ requestConfig.headers[Authorization] = "Bearer $accessToken"
+ }
+ }
+ }
protected inline fun request(requestConfig: RequestConfig): ApiResponse {
val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.")
+ // take authMethod from operation
+ updateAuthParams(requestConfig)
+
val url = httpUrl.newBuilder()
.addEncodedPathSegments(requestConfig.path.trimStart('/'))
.apply {
diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
index 79a7e26341..76b0557385 100644
--- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
+++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
@@ -81,6 +81,10 @@ import {
GetOpenApiSpecV1Endpoint,
IGetOpenApiSpecV1EndpointOptions,
} from "./web-services/get-open-api-spec-v1-endpoint";
+import {
+ GetHealthcheckV1Endpoint,
+ IGetHealthcheckV1EndpointOptions,
+} from "./web-services/get-healthcheck-v1-endpoint";
export interface IApiServerConstructorOptions {
readonly pluginManagerOptions?: { pluginsPath: string };
@@ -640,6 +644,15 @@ export class ApiServer {
const { logLevel } = this.options.config;
const pluginRegistry = await this.getOrInitPluginRegistry();
+ {
+ const opts: IGetHealthcheckV1EndpointOptions = {
+ process: global.process,
+ logLevel,
+ };
+ const endpoint = new GetHealthcheckV1Endpoint(opts);
+ await registerWebServiceEndpoint(app, endpoint);
+ }
+
{
const oasPath = OAS.paths["/api/v1/api-server/get-open-api-spec"];
@@ -657,23 +670,6 @@ export class ApiServer {
await registerWebServiceEndpoint(app, endpoint);
}
- const healthcheckHandler = (req: Request, res: Response) => {
- res.json({
- success: true,
- createdAt: new Date(),
- memoryUsage: process.memoryUsage(),
- });
- };
-
- const { "/api/v1/api-server/healthcheck": oasPath } = OAS.paths;
- const { http } = oasPath.get["x-hyperledger-cacti"];
- const { path: httpPath, verbLowerCase: httpVerb } = http;
- if (!isExpressHttpVerbMethodName(httpVerb)) {
- const eMsg = `${fnTag} Invalid HTTP verb "${httpVerb}" in cmd-api-server OpenAPI specification for HTTP path: "${httpPath}"`;
- throw new RuntimeError(eMsg);
- }
- app[httpVerb](httpPath, healthcheckHandler);
-
this.wsApi.on("connection", (socket: SocketIoSocket) => {
const { id } = socket;
const transport = socket.conn.transport.name; // in most cases, "polling"
diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts
index 6610e56b53..5d8ed9864a 100644
--- a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts
+++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts
@@ -128,6 +128,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
+ // authentication bearerTokenAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -157,6 +161,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
+ // authentication bearerTokenAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -187,6 +195,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
+ // authentication bearerTokenAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
diff --git a/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-healthcheck-v1-endpoint.ts b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-healthcheck-v1-endpoint.ts
new file mode 100644
index 0000000000..f1ee542cda
--- /dev/null
+++ b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-healthcheck-v1-endpoint.ts
@@ -0,0 +1,117 @@
+import { StatusCodes } from "http-status-codes";
+import type { Express, Request, Response } from "express";
+
+import {
+ Checks,
+ IAsyncProvider,
+ Logger,
+ LoggerProvider,
+ LogLevelDesc,
+} from "@hyperledger/cactus-common";
+import {
+ IEndpointAuthzOptions,
+ IExpressRequestHandler,
+ IWebServiceEndpoint,
+} from "@hyperledger/cactus-core-api";
+import {
+ handleRestEndpointException,
+ IHandleRestEndpointExceptionOptions,
+ registerWebServiceEndpoint,
+} from "@hyperledger/cactus-core";
+
+import OAS from "../../json/openapi.json";
+
+export interface IGetHealthcheckV1EndpointOptions {
+ readonly logLevel?: LogLevelDesc;
+ readonly process: NodeJS.Process;
+}
+
+export class GetHealthcheckV1Endpoint implements IWebServiceEndpoint {
+ public static readonly CLASS_NAME = "GetHealthcheckV1Endpoint";
+
+ private readonly log: Logger;
+
+ private readonly process: NodeJS.Process;
+
+ public get className(): string {
+ return GetHealthcheckV1Endpoint.CLASS_NAME;
+ }
+
+ constructor(public readonly opts: IGetHealthcheckV1EndpointOptions) {
+ const fnTag = `${this.className}#constructor()`;
+ Checks.truthy(opts, `${fnTag} arg opts`);
+ Checks.truthy(opts.process, `${fnTag} arg opts.process`);
+
+ this.process = opts.process;
+
+ const level = this.opts.logLevel || "INFO";
+ const label = this.className;
+ this.log = LoggerProvider.getOrCreate({ level, label });
+ }
+
+ public getAuthorizationOptionsProvider(): IAsyncProvider {
+ return {
+ get: async () => ({
+ isProtected: true,
+ requiredRoles: this.oasPath.get.security[0].bearerTokenAuth,
+ }),
+ };
+ }
+
+ public getExpressRequestHandler(): IExpressRequestHandler {
+ return this.handleRequest.bind(this);
+ }
+
+ public get oasPath(): (typeof OAS.paths)["/api/v1/api-server/healthcheck"] {
+ return OAS.paths["/api/v1/api-server/healthcheck"];
+ }
+
+ public getPath(): string {
+ return this.oasPath.get["x-hyperledger-cacti"].http.path;
+ }
+
+ public getVerbLowerCase(): string {
+ return this.oasPath.get["x-hyperledger-cacti"].http.verbLowerCase;
+ }
+
+ public getOperationId(): string {
+ return this.oasPath.get.operationId;
+ }
+
+ public async registerExpress(
+ expressApp: Express,
+ ): Promise {
+ await registerWebServiceEndpoint(expressApp, this);
+ return this;
+ }
+
+ async handleRequest(_req: Request, res: Response): Promise {
+ const fnTag = `${this.className}#handleRequest()`;
+ const verbUpper = this.getVerbLowerCase().toUpperCase();
+ const reqTag = `${verbUpper} ${this.getPath()}`;
+ this.log.debug(reqTag);
+
+ try {
+ const memoryUsage = this.process.memoryUsage();
+ const createdAt = new Date();
+ const body = {
+ success: true,
+ createdAt,
+ memoryUsage,
+ };
+ res.json(body).status(StatusCodes.OK);
+ } catch (error) {
+ const { log } = this;
+ const errorMsg = `${fnTag} request handler fn crashed for: ${reqTag}`;
+
+ const ctx: Readonly = {
+ errorMsg,
+ log,
+ error,
+ res,
+ };
+
+ await handleRestEndpointException(ctx);
+ }
+ }
+}
diff --git a/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts
index 79b3e6d4b0..c71993819b 100644
--- a/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts
+++ b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts
@@ -3,8 +3,15 @@ import {
IGetOpenApiSpecV1EndpointBaseOptions,
} from "@hyperledger/cactus-core";
-import { Checks, LogLevelDesc } from "@hyperledger/cactus-common";
-import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api";
+import {
+ Checks,
+ IAsyncProvider,
+ LogLevelDesc,
+} from "@hyperledger/cactus-common";
+import {
+ IEndpointAuthzOptions,
+ IWebServiceEndpoint,
+} from "@hyperledger/cactus-core-api";
import OAS from "../../json/openapi.json";
@@ -34,4 +41,13 @@ export class GetOpenApiSpecV1Endpoint
const fnTag = `${this.className}#constructor()`;
Checks.truthy(options, `${fnTag} arg options`);
}
+
+ public getAuthorizationOptionsProvider(): IAsyncProvider {
+ return {
+ get: async () => ({
+ isProtected: true,
+ requiredRoles: this.opts.oasPath.get.security[0].bearerTokenAuth,
+ }),
+ };
+ }
}
diff --git a/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts b/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts
index 2babf8ff13..70bd10444d 100644
--- a/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts
+++ b/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts
@@ -104,7 +104,11 @@ const createTestInfrastructure = async (opts: {
const grpcHost = `${addressInfoGrpc.address}:${addressInfoGrpc.port}`;
- const jwtPayload = { name: "Peter", location: "Albertirsa" };
+ const jwtPayload = {
+ name: "Peter",
+ location: "London",
+ scope: "read:spec",
+ };
const validJwt = await new SignJWT(jwtPayload)
.setProtectedHeader({ alg: "RS256" })
.setIssuer(expressJwtOptions.issuer)
diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts
index 2e0d8c7e57..555520c3d6 100644
--- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts
+++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts
@@ -87,7 +87,11 @@ describe(testCase, () => {
try {
expect(expressJwtOptions).toBeTruthy();
- const jwtPayload = { name: "Peter", location: "London" };
+ const jwtPayload = {
+ name: "Peter",
+ location: "London",
+ scope: "read:health",
+ };
const tokenGood = await new SignJWT(jwtPayload)
.setProtectedHeader({
alg: "RS256",
@@ -100,7 +104,6 @@ describe(testCase, () => {
const startResponse = apiServer.start();
await expect(startResponse).not.toReject;
expect(startResponse).toBeTruthy();
-
const addressInfoApi = (await startResponse).addressInfoApi;
const protocol = apiSrvOpts.apiTlsEnabled ? "https" : "http";
const { address, port } = addressInfoApi;
diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts
index 38b636d7c4..2fb27ed2fe 100644
--- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts
+++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts
@@ -84,7 +84,11 @@ describe("cmd-api-server:ApiServer", () => {
const { address, port } = addressInfoApi;
apiHost = `${protocol}://${address}:${port}`;
- const jwtPayload = { name: "Peter", location: "Albertirsa" };
+ const jwtPayload = {
+ name: "Peter",
+ location: "London",
+ scope: "read:health",
+ };
const validJwt = await new SignJWT(jwtPayload)
.setProtectedHeader({ alg: "RS256" })
.setIssuer(expressJwtOptions.issuer)
diff --git a/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts
index 5b44fa205a..70ae5e0a8d 100644
--- a/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts
+++ b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts
@@ -101,7 +101,11 @@ describe("cmd-api-server:getOpenApiSpecV1Endpoint", () => {
grpcHost = `${addressInfoGrpc.address}:${addressInfoGrpc.port}`;
- const jwtPayload = { name: "Peter", location: "Albertirsa" };
+ const jwtPayload = {
+ name: "Peter",
+ location: "London",
+ scope: "read:spec",
+ };
const validJwt = await new SignJWT(jwtPayload)
.setProtectedHeader({ alg: "RS256" })
.setIssuer(expressJwtOptions.issuer)
diff --git a/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-oauth2-scopes.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-oauth2-scopes.test.ts
new file mode 100644
index 0000000000..c5f69dc4f7
--- /dev/null
+++ b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-oauth2-scopes.test.ts
@@ -0,0 +1,274 @@
+import {
+ ApiServer,
+ ApiServerApiClient,
+ ApiServerApiClientConfiguration,
+ AuthorizationProtocol,
+ ConfigService,
+ IAuthorizationConfig,
+} from "../../../main/typescript/public-api";
+import {
+ IJoseFittingJwtParams,
+ LogLevelDesc,
+} from "@hyperledger/cactus-common";
+import { PluginRegistry } from "@hyperledger/cactus-core";
+import { Constants } from "@hyperledger/cactus-core-api";
+import type { AuthorizeOptions as SocketIoJwtOptions } from "@thream/socketio-jwt";
+import type { Params as ExpressJwtOptions } from "express-jwt";
+import "jest-extended";
+import { SignJWT, exportSPKI, generateKeyPair } from "jose";
+import path from "path";
+import { v4 as uuidv4 } from "uuid";
+
+describe("cmd-api-server:getOpenApiSpecV1Endpoint", () => {
+ const logLevel: LogLevelDesc = "INFO";
+ let apiServer: ApiServer;
+ let apiClient: ApiServerApiClient;
+ let jwtKeyPair: { publicKey: CryptoKey; privateKey: CryptoKey };
+ let expressJwtOptions: ExpressJwtOptions & IJoseFittingJwtParams;
+
+ afterAll(async () => await apiServer.shutdown());
+
+ beforeAll(async () => {
+ jwtKeyPair = await generateKeyPair("RS256", { modulusLength: 4096 });
+ const jwtPublicKey = await exportSPKI(jwtKeyPair.publicKey);
+
+ expressJwtOptions = {
+ algorithms: ["RS256"],
+ secret: jwtPublicKey,
+ audience: uuidv4(),
+ issuer: uuidv4(),
+ };
+
+ const socketIoJwtOptions: SocketIoJwtOptions = {
+ secret: jwtPublicKey,
+ algorithms: ["RS256"],
+ };
+ expect(expressJwtOptions).toBeTruthy();
+
+ const authorizationConfig: IAuthorizationConfig = {
+ unprotectedEndpointExemptions: [],
+ expressJwtOptions,
+ socketIoJwtOptions,
+ socketIoPath: Constants.SocketIoConnectionPathV1,
+ };
+
+ const pluginsPath = path.join(
+ __dirname,
+ "../../../../../../", // walk back up to the project root
+ ".tmp/test/test-cmd-api-server/get-open-api-spec-v1-endpoint_test/", // the dir path from the root
+ uuidv4(), // then a random directory to ensure proper isolation
+ );
+ const pluginManagerOptionsJson = JSON.stringify({ pluginsPath });
+
+ const pluginRegistry = new PluginRegistry({ logLevel });
+
+ const configService = new ConfigService();
+
+ const apiSrvOpts = await configService.newExampleConfig();
+ apiSrvOpts.logLevel = logLevel;
+ apiSrvOpts.pluginManagerOptionsJson = pluginManagerOptionsJson;
+ apiSrvOpts.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN;
+ apiSrvOpts.authorizationConfigJson = authorizationConfig;
+ apiSrvOpts.configFile = "";
+ apiSrvOpts.apiCorsDomainCsv = "*";
+ apiSrvOpts.apiPort = 0;
+ apiSrvOpts.cockpitPort = 0;
+ apiSrvOpts.grpcPort = 0;
+ apiSrvOpts.crpcPort = 0;
+ apiSrvOpts.apiTlsEnabled = false;
+ apiSrvOpts.grpcMtlsEnabled = false;
+ apiSrvOpts.plugins = [];
+
+ const config = await configService.newExampleConfigConvict(apiSrvOpts);
+
+ apiServer = new ApiServer({
+ config: config.getProperties(),
+ pluginRegistry,
+ });
+
+ apiServer.initPluginRegistry({ pluginRegistry });
+ const startResponsePromise = apiServer.start();
+ await expect(startResponsePromise).toResolve();
+ const startResponse = await startResponsePromise;
+ expect(startResponse).toBeTruthy();
+
+ const { addressInfoApi } = await startResponsePromise;
+ const protocol = apiSrvOpts.apiTlsEnabled ? "https" : "http";
+ const { address, port } = addressInfoApi;
+ const apiHost = `${protocol}://${address}:${port}`;
+
+ const jwtPayload = { name: "Peter", location: "Albertirsa" };
+ const validJwt = await new SignJWT(jwtPayload)
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer(expressJwtOptions.issuer)
+ .setAudience(expressJwtOptions.audience)
+ .sign(jwtKeyPair.privateKey);
+ expect(validJwt).toBeTruthy();
+
+ const validBearerToken = `Bearer ${validJwt}`;
+ expect(validBearerToken).toBeTruthy();
+
+ apiClient = new ApiServerApiClient(
+ new ApiServerApiClientConfiguration({
+ basePath: apiHost,
+ baseOptions: { headers: { Authorization: validBearerToken } },
+ logLevel,
+ }),
+ );
+ });
+
+ it("HTTP - allows request execution with a valid JWT Token", async () => {
+ const jwtPayload = { scope: "read:spec" };
+ const validJwt = await new SignJWT(jwtPayload)
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer(expressJwtOptions.issuer)
+ .setAudience(expressJwtOptions.audience)
+ .sign(jwtKeyPair.privateKey);
+
+ const validBearerToken = `Bearer ${validJwt}`;
+ expect(validBearerToken).toBeTruthy();
+
+ const res3Promise = apiClient.getOpenApiSpecV1({
+ headers: { Authorization: validBearerToken },
+ });
+
+ await expect(res3Promise).resolves.toHaveProperty("data.openapi");
+ const res3 = await res3Promise;
+ expect(res3.status).toEqual(200);
+ expect(res3.data).toBeTruthy();
+ });
+
+ it("HTTP - rejects request with an valid JWT but incorrect scope", async () => {
+ const jwtPayload = { scope: "red:specs" };
+ const validJwt = await new SignJWT(jwtPayload)
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer(expressJwtOptions.issuer)
+ .setAudience(expressJwtOptions.audience)
+ .sign(jwtKeyPair.privateKey);
+
+ const validBearerToken = `Bearer ${validJwt}`;
+ expect(validBearerToken).toBeTruthy();
+
+ const res3Promise = apiClient.getOpenApiSpecV1({
+ headers: { Authorization: validBearerToken },
+ });
+ await expect(res3Promise).rejects.toThrow();
+ try {
+ await res3Promise;
+ } catch (error) {
+ const statusCode = error.response?.status;
+ console.log(
+ `Request failed with valid JWT but incorrect scope status code: ${statusCode}`,
+ );
+ }
+ });
+
+ it("HTTP - rejects request with an invalid JWT", async () => {
+ const { privateKey: otherPrivateKey } = await generateKeyPair("RS256");
+ const invalidJwt = await new SignJWT({ scope: "invalid:scope" })
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer("invalid-issuer")
+ .setAudience("invalid-audience")
+ .sign(otherPrivateKey);
+
+ const invalidBearerToken = `Bearer ${invalidJwt}`;
+ expect(invalidBearerToken).toBeTruthy();
+
+ const res3Promise = apiClient.getOpenApiSpecV1({
+ headers: { Authorization: invalidBearerToken },
+ });
+ await expect(res3Promise).rejects.toThrow(
+ "Request failed with status code 401",
+ );
+ });
+
+ it("HTTP - allows health check execution with a valid JWT Token", async () => {
+ const jwtPayload = { scope: "read:health" };
+ const validJwt = await new SignJWT(jwtPayload)
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer(expressJwtOptions.issuer)
+ .setAudience(expressJwtOptions.audience)
+ .sign(jwtKeyPair.privateKey);
+
+ const validBearerToken = `Bearer ${validJwt}`;
+ expect(validBearerToken).toBeTruthy();
+
+ const resPromise = apiClient.getHealthCheckV1({
+ headers: { Authorization: validBearerToken },
+ });
+
+ await expect(resPromise).resolves.toHaveProperty("data");
+ const res = await resPromise;
+ expect(res.status).toEqual(200);
+ expect(res.data).toBeTruthy();
+ });
+
+ it("HTTP - rejects health check execution with an invalid JWT", async () => {
+ const { privateKey: otherPrivateKey } = await generateKeyPair("RS256");
+ const invalidJwt = await new SignJWT({ scope: "invalid:scope" })
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer("invalid-issuer")
+ .setAudience("invalid-audience")
+ .sign(otherPrivateKey);
+
+ const invalidBearerToken = `Bearer ${invalidJwt}`;
+ expect(invalidBearerToken).toBeTruthy();
+
+ const resPromise = apiClient.getHealthCheckV1({
+ headers: { Authorization: invalidBearerToken },
+ });
+ await expect(resPromise).rejects.toThrow();
+
+ try {
+ await resPromise;
+ } catch (error) {
+ const statusCode = error.response?.status;
+ console.log(`Request failed with status code: ${statusCode}`);
+ }
+ });
+
+ it("HTTP - allows Prometheus metrics execution with a valid JWT Token", async () => {
+ const jwtPayload = { scope: "read:metrics" };
+ const validJwt = await new SignJWT(jwtPayload)
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer(expressJwtOptions.issuer)
+ .setAudience(expressJwtOptions.audience)
+ .sign(jwtKeyPair.privateKey);
+
+ const validBearerToken = `Bearer ${validJwt}`;
+ expect(validBearerToken).toBeTruthy();
+
+ const resPromise = apiClient.getPrometheusMetricsV1({
+ headers: { Authorization: validBearerToken },
+ });
+
+ await expect(resPromise).resolves.toHaveProperty("data");
+ const res = await resPromise;
+ expect(res.status).toEqual(200);
+ expect(res.data).toBeTruthy();
+ });
+
+ it("HTTP - rejects Prometheus metrics execution with an invalid JWT", async () => {
+ const { privateKey: otherPrivateKey } = await generateKeyPair("RS256");
+ const invalidJwt = await new SignJWT({ scope: "invalid:scope" })
+ .setProtectedHeader({ alg: "RS256" })
+ .setIssuer("invalid-issuer")
+ .setAudience("invalid-audience")
+ .sign(otherPrivateKey);
+
+ const invalidBearerToken = `Bearer ${invalidJwt}`;
+ expect(invalidBearerToken).toBeTruthy();
+
+ const resPromise = apiClient.getPrometheusMetricsV1({
+ headers: { Authorization: invalidBearerToken },
+ });
+ await expect(resPromise).rejects.toThrow();
+
+ try {
+ await resPromise;
+ } catch (error) {
+ const statusCode = error.response?.status;
+ console.log(`Request failed with status code: ${statusCode}`);
+ }
+ });
+});