diff --git a/src/node/handler/RestAPI.ts b/src/node/handler/RestAPI.ts index 1b0d1d09d1d..1e3427eb75c 100644 --- a/src/node/handler/RestAPI.ts +++ b/src/node/handler/RestAPI.ts @@ -192,6 +192,16 @@ const prepareDefinition = (mapping: Map>, "in": string }, + "apiKeyAlias"?: { + "type": string, + "name": string, + "in": string + }, + "apiKeyHeader"?: { + "type": string, + "name": string, + "in": string + }, "sso"?: { "type": string, "flows": { @@ -255,10 +265,20 @@ const prepareDefinition = (mapping: Map>, } if (authenticationMethod === "apikey") { + definitions.components.securitySchemes.apiKeyAlias = { + type: "apiKey", + name: "api_key", + in: "query", + }; + definitions.components.securitySchemes.apiKeyHeader = { + type: "apiKey", + name: "apikey", + in: "header", + }; definitions.security = [ - { - "apiKey": [] - } + {"apiKey": []}, + {"apiKeyAlias": []}, + {"apiKeyHeader": []}, ] } else if (authenticationMethod === "sso") { definitions.components.securitySchemes.sso = { diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index 519b5ec78f2..e07daf6d86b 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -482,26 +482,44 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) responses: { ...defaultResponses, }, - securitySchemes: { - openid: { - type: "oauth2", - flows: { - authorizationCode: { - authorizationUrl: settings.sso.issuer+"/oidc/auth", - tokenUrl: settings.sso.issuer+"/oidc/token", - scopes: { - openid: "openid", - profile: "profile", - email: "email", - admin: "admin" - } - } + securitySchemes: {} as Record, + }, + security: [] as Array>, + }; + + if (settings.authenticationMethod === 'apikey') { + definition.components.securitySchemes.apiKey = { + type: 'apiKey', name: 'apikey', in: 'query', + }; + definition.components.securitySchemes.apiKeyAlias = { + type: 'apiKey', name: 'api_key', in: 'query', + }; + definition.components.securitySchemes.apiKeyHeader = { + type: 'apiKey', name: 'apikey', in: 'header', + }; + definition.security = [ + {apiKey: []}, + {apiKeyAlias: []}, + {apiKeyHeader: []}, + ]; + } else { + definition.components.securitySchemes.openid = { + type: 'oauth2', + flows: { + authorizationCode: { + authorizationUrl: settings.sso.issuer + '/oidc/auth', + tokenUrl: settings.sso.issuer + '/oidc/token', + scopes: { + openid: 'openid', + profile: 'profile', + email: 'email', + admin: 'admin', }, }, }, - }, - security: [{openid: []}], - }; + }; + definition.security = [{openid: []}]; + } // build operations for (const funcName of Object.keys(apiHandler.version[version])) { @@ -566,14 +584,16 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { for (const style of [APIPathStyle.FLAT, APIPathStyle.REST]) { const apiRoot = getApiRootForVersion(version, style); - // generate openapi definition for this API version + // generate openapi definition for this API version (used for openapi-backend routing) const definition = generateDefinitionForVersion(version, style); - // serve version specific openapi definition + // serve version specific openapi definition; regenerate per request so runtime + // settings (e.g. authenticationMethod) are reflected app.get(`${apiRoot}/openapi.json`, (req:any, res:any) => { // For openapi definitions, wide CORS is probably fine res.header('Access-Control-Allow-Origin', '*'); - res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); + const liveDefinition = generateDefinitionForVersion(version, style); + res.json({...liveDefinition, servers: [generateServerForApiVersion(apiRoot, req)]}); }); // serve latest openapi definition file under /api/openapi.json @@ -581,7 +601,8 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { if (isLatestAPIVersion) { app.get(`/${style}/openapi.json`, (req:any, res:any) => { res.header('Access-Control-Allow-Origin', '*'); - res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); + const liveDefinition = generateDefinitionForVersion(version, style); + res.json({...liveDefinition, servers: [generateServerForApiVersion(apiRoot, req)]}); }); } diff --git a/src/tests/backend/specs/api/api.ts b/src/tests/backend/specs/api/api.ts index bab70b47ca6..53e2e84c976 100644 --- a/src/tests/backend/specs/api/api.ts +++ b/src/tests/backend/specs/api/api.ts @@ -10,6 +10,7 @@ const common = require('../../common'); const validateOpenAPI = require('openapi-schema-validation').validate; +import settings from '../../../../node/utils/Settings'; let agent: any; let apiVersion = 1; @@ -54,4 +55,61 @@ describe(__filename, function () { } }); }); + + describe('security schemes with authenticationMethod=apikey', function () { + let originalAuthMethod: string; + + before(function () { + originalAuthMethod = settings.authenticationMethod; + settings.authenticationMethod = 'apikey'; + }); + + after(function () { + settings.authenticationMethod = originalAuthMethod; + }); + + it('/api-docs.json documents apikey query param (primary name)', async function () { + const res = await agent.get('/api-docs.json').expect(200); + const schemes = res.body.components.securitySchemes; + const apiKeyQuery = Object.values(schemes).find( + (s: any) => s.type === 'apiKey' && s.in === 'query' && s.name === 'apikey'); + if (!apiKeyQuery) { + throw new Error(`Expected apiKey query param 'apikey' in securitySchemes: ` + + `${JSON.stringify(schemes)}`); + } + }); + + it('/api-docs.json documents api_key query param alias', async function () { + const res = await agent.get('/api-docs.json').expect(200); + const schemes = res.body.components.securitySchemes; + const apiKeyQueryAlias = Object.values(schemes).find( + (s: any) => s.type === 'apiKey' && s.in === 'query' && s.name === 'api_key'); + if (!apiKeyQueryAlias) { + throw new Error(`Expected apiKey query param 'api_key' in securitySchemes: ` + + `${JSON.stringify(schemes)}`); + } + }); + + it('/api-docs.json documents apikey header', async function () { + const res = await agent.get('/api-docs.json').expect(200); + const schemes = res.body.components.securitySchemes; + const apiKeyHeader = Object.values(schemes).find( + (s: any) => s.type === 'apiKey' && s.in === 'header' && s.name === 'apikey'); + if (!apiKeyHeader) { + throw new Error(`Expected apiKey header 'apikey' in securitySchemes: ` + + `${JSON.stringify(schemes)}`); + } + }); + + it('/api/openapi.json exposes apiKey security in apikey mode', async function () { + this.timeout(15000); + const res = await agent.get('/api/openapi.json').expect(200); + const schemes = res.body.components.securitySchemes; + const hasApiKey = Object.values(schemes).some((s: any) => s.type === 'apiKey'); + if (!hasApiKey) { + throw new Error(`Expected at least one apiKey securityScheme in ` + + `/api/openapi.json, got: ${JSON.stringify(schemes)}`); + } + }); + }); });