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 ba8f8c9d1e3..1f02c475617 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
@@ -15,6 +15,7 @@ Install the following dependencies:
```shell
go get github.com/stretchr/testify/assert
+go get golang.org/x/oauth2
go get golang.org/x/net/context
```
@@ -91,7 +92,37 @@ Class | Method | HTTP request | Description
## Documentation For Authorization
-Endpoints do not require authorization.
+
+Authentication schemes defined for the API:
+### OAuth2
+
+
+- **Type**: OAuth
+- **Flow**: accessCode
+- **Authorization URL**: http://0.0.0.0:4000/oauth/authorize
+- **Scopes**:
+ - **read:health**: Read health information
+ - **read:metrics**: Read metrics information
+ - **read:spec**: Read OpenAPI specification
+
+Example
+
+```golang
+auth := context.WithValue(context.Background(), sw.ContextAccessToken, "ACCESSTOKENSTRING")
+r, err := client.Service.Operation(auth, args)
+```
+
+Or via OAuth2 module to automatically refresh tokens and perform user authentication.
+
+```golang
+import "golang.org/x/oauth2"
+
+/* Perform OAuth2 round trip request and obtain a token */
+
+tokenSource := oauth2cfg.TokenSource(createContext(httpClient), &token)
+auth := context.WithValue(oauth2.NoContext, sw.ContextOAuth2, tokenSource)
+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 eb4edd3e058..18bd5be805e 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-rc.3
servers:
- url: /
+security:
+- OAuth2:
+ - read:health
+ - read:metrics
+ - read:spec
paths:
/api/v1/api-server/healthcheck:
get:
@@ -21,6 +26,11 @@ paths:
schema:
$ref: '#/components/schemas/HealthCheckResponse'
description: OK
+ "401":
+ description: Unauthorized
+ security:
+ - OAuth2:
+ - read:health
summary: Can be used to verify liveness of an API server instance
x-hyperledger-cacti:
http:
@@ -37,6 +47,11 @@ paths:
schema:
$ref: '#/components/schemas/PrometheusExporterMetricsResponse'
description: OK
+ "401":
+ description: Unauthorized
+ security:
+ - OAuth2:
+ - read:metrics
summary: Get the Prometheus Metrics
x-hyperledger-cacti:
http:
@@ -54,6 +69,11 @@ paths:
schema:
$ref: '#/components/schemas/GetOpenApiSpecV1EndpointResponse'
description: OK
+ "401":
+ description: Unauthorized
+ security:
+ - OAuth2:
+ - read:spec
x-hyperledger-cacti:
http:
verbLowerCase: get
@@ -127,3 +147,14 @@ components:
GetOpenApiSpecV1EndpointResponse:
nullable: false
type: string
+ securitySchemes:
+ OAuth2:
+ flows:
+ authorizationCode:
+ authorizationUrl: http://0.0.0.0:4000/oauth/authorize
+ scopes:
+ read:health: Read health information
+ read:metrics: Read metrics information
+ read:spec: Read OpenAPI specification
+ tokenUrl: http://0.0.0.0:4000/oauth/token
+ type: oauth2
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 0b8adb637e5..779dbcbe807 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
@@ -32,6 +32,7 @@ import (
"time"
"unicode/utf8"
+ "golang.org/x/oauth2"
)
var (
@@ -410,6 +411,17 @@ func (c *APIClient) prepareRequest(
// Walk through any authentication.
+ // OAuth2 authentication
+ if tok, ok := ctx.Value(ContextOAuth2).(oauth2.TokenSource); ok {
+ // We were able to grab an oauth2 token from the context
+ var latestToken *oauth2.Token
+ if latestToken, err = tok.Token(); err != nil {
+ return nil, err
+ }
+
+ latestToken.SetAuthHeader(localVarRequest)
+ }
+
}
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 9d761b0c7bc..3400d795ba7 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 (
+ // ContextOAuth2 takes an oauth2.TokenSource as authentication for the request.
+ ContextOAuth2 = contextKey("token")
+
// ContextServerIndex uses a server configuration from the index.
ContextServerIndex = contextKey("serverIndex")
diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.mod b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.mod
index cf6f7e9b4ab..18881d23d4a 100644
--- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.mod
+++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.mod
@@ -3,4 +3,5 @@ module github.com/hyperledger/cactus-cmd-api-server/src/main/go/generated/openap
go 1.18
require (
+ golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
)
diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.sum b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.sum
index c966c8ddfd0..734252e6815 100644
--- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.sum
+++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/go.sum
@@ -4,6 +4,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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 8ca9fbf2202..698930d62e8 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,29 @@
"type": "string",
"nullable": false
}
+ },
+ "securitySchemes": {
+ "OAuth2": {
+ "type": "oauth2",
+ "flows": {
+ "authorizationCode": {
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "read:health": "Read health information",
+ "read:metrics": "Read metrics information",
+ "read:spec": "Read OpenAPI specification"
+ }
+ }
+ }
+ }
}
},
+ "security": [
+ {
+ "OAuth2": ["read:health", "read:metrics", "read:spec"]
+ }
+ ],
"paths": {
"/api/v1/api-server/healthcheck": {
"get": {
@@ -101,8 +122,16 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
}
- }
+ },
+ "security": [
+ {
+ "OAuth2": ["read:health"]
+ }
+ ]
}
},
"/api/v1/api-server/get-prometheus-exporter-metrics": {
@@ -126,8 +155,16 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
}
- }
+ },
+ "security": [
+ {
+ "OAuth2": ["read:metrics"]
+ }
+ ]
}
},
"/api/v1/api-server/get-open-api-spec": {
@@ -151,8 +188,16 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
}
- }
+ },
+ "security": [
+ {
+ "OAuth2": ["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 8ca9fbf2202..698930d62e8 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,29 @@
"type": "string",
"nullable": false
}
+ },
+ "securitySchemes": {
+ "OAuth2": {
+ "type": "oauth2",
+ "flows": {
+ "authorizationCode": {
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "read:health": "Read health information",
+ "read:metrics": "Read metrics information",
+ "read:spec": "Read OpenAPI specification"
+ }
+ }
+ }
+ }
}
},
+ "security": [
+ {
+ "OAuth2": ["read:health", "read:metrics", "read:spec"]
+ }
+ ],
"paths": {
"/api/v1/api-server/healthcheck": {
"get": {
@@ -101,8 +122,16 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
}
- }
+ },
+ "security": [
+ {
+ "OAuth2": ["read:health"]
+ }
+ ]
}
},
"/api/v1/api-server/get-prometheus-exporter-metrics": {
@@ -126,8 +155,16 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
}
- }
+ },
+ "security": [
+ {
+ "OAuth2": ["read:metrics"]
+ }
+ ]
}
},
"/api/v1/api-server/get-open-api-spec": {
@@ -151,8 +188,16 @@
}
}
}
+ },
+ "401": {
+ "description": "Unauthorized"
}
- }
+ },
+ "security": [
+ {
+ "OAuth2": ["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 8321b31aab0..809d1acd7fb 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,16 @@ Class | Method | HTTP request | Description
## Documentation for Authorization
-Endpoints do not require authorization.
+
+Authentication schemes defined for the API:
+
+### OAuth2
+
+- **Type**: OAuth
+- **Flow**: accessCode
+- **Authorization URL**: http://0.0.0.0:4000/oauth/authorize
+- **Scopes**:
+ - read:health: Read health information
+ - read:metrics: Read metrics information
+ - read:spec: Read OpenAPI specification
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 d05dc1394b8..36315922220 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 ea4b7b65935..79a51064818 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/generated/openapi/typescript-axios/api.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts
index c31c56ec0eb..24e646c87af 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 OAuth2 required
+ // oauth required
+ await setOAuthToObject(localVarHeaderParameter, "OAuth2", ["read:health"], configuration)
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -157,6 +161,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
+ // authentication OAuth2 required
+ // oauth required
+ await setOAuthToObject(localVarHeaderParameter, "OAuth2", ["read:spec"], configuration)
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -187,6 +195,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
+ // authentication OAuth2 required
+ // oauth required
+ await setOAuthToObject(localVarHeaderParameter, "OAuth2", ["read:metrics"], configuration)
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
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 00000000000..40145339046
--- /dev/null
+++ b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-oauth2-scopes.test.ts
@@ -0,0 +1,209 @@
+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";
+
+interface IExpectedScopes {
+ [key: string]: string;
+}
+
+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 - returns the OpenAPI spec .json document of the API server itself", async () => {
+ const res1Promise = apiClient.getOpenApiSpecV1();
+ await expect(res1Promise).resolves.toHaveProperty("data.openapi");
+ const res1 = await res1Promise;
+ expect(res1.status).toEqual(200);
+ expect(res1.data).toBeTruthy();
+
+ console.log("Response data type:", typeof res1.data);
+ console.log("Response data:", res1.data);
+
+ let openApiSpec;
+ try {
+ openApiSpec =
+ typeof res1.data === "string" ? JSON.parse(res1.data) : res1.data;
+ } catch (error) {
+ throw new Error(`Failed to parse OpenAPI spec: ${error.message}`);
+ }
+
+ expect(openApiSpec).toHaveProperty("components");
+ expect(openApiSpec.components).toHaveProperty("securitySchemes");
+
+ const securitySchemes = openApiSpec.components.securitySchemes;
+ expect(securitySchemes).toBeObject();
+
+ const expectedScopes: IExpectedScopes = {
+ "read:health": "Read health information",
+ "read:metrics": "Read metrics information",
+ "read:spec": "Read OpenAPI specification",
+ };
+
+ const securitySchemeNames = Object.keys(securitySchemes);
+
+ securitySchemeNames.forEach((schemeName) => {
+ const scheme = securitySchemes[schemeName];
+ expect(scheme).toHaveProperty("flows");
+ const flows = scheme.flows;
+ expect(flows).toHaveProperty("authorizationCode");
+ const scopes = flows.authorizationCode.scopes as IExpectedScopes;
+
+ Object.keys(expectedScopes).forEach((scope) => {
+ expect(scopes).toHaveProperty(scope);
+ expect(scopes[scope]).toEqual(expectedScopes[scope]);
+ });
+ });
+ });
+
+ 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 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",
+ );
+ });
+});