diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index 92101da86b..eac8f44c2a 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -197,6 +197,11 @@ export interface ParseOptions { */ allowAPIKeyAuth?: boolean; + /** + * If true, the parser will allow Bearer Token authentication in the spec file. + */ + allowBearerTokenAuth?: boolean; + /** * If true, the parser will allow multiple parameters in the spec file. Teams AI project would ignore this parameters and always true */ @@ -243,6 +248,6 @@ export interface ListAPIResult { } export interface AuthInfo { - authSchema: OpenAPIV3.SecuritySchemeObject; + authScheme: OpenAPIV3.SecuritySchemeObject; name: string; } diff --git a/packages/spec-parser/src/manifestUpdater.ts b/packages/spec-parser/src/manifestUpdater.ts index d3cabba943..6b6e403ecb 100644 --- a/packages/spec-parser/src/manifestUpdater.ts +++ b/packages/spec-parser/src/manifestUpdater.ts @@ -225,9 +225,8 @@ export class ManifestUpdater { }; if (authInfo) { - let auth = authInfo.authSchema; - if (Utils.isAPIKeyAuth(auth)) { - auth = auth as OpenAPIV3.ApiKeySecurityScheme; + const auth = authInfo.authScheme; + if (Utils.isAPIKeyAuth(auth) || Utils.isBearerTokenAuth(auth)) { const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName( `${authInfo.name}_${ConstantString.RegistrationIdPostfix}` ); diff --git a/packages/spec-parser/src/specParser.browser.ts b/packages/spec-parser/src/specParser.browser.ts index d96106d198..01a5af6e90 100644 --- a/packages/spec-parser/src/specParser.browser.ts +++ b/packages/spec-parser/src/specParser.browser.ts @@ -37,6 +37,7 @@ export class SpecParser { allowSwagger: false, allowAPIKeyAuth: false, allowMultipleParameters: false, + allowBearerTokenAuth: false, allowOauth2: false, allowMethods: ["get", "post"], projectType: ProjectType.SME, diff --git a/packages/spec-parser/src/specParser.ts b/packages/spec-parser/src/specParser.ts index f59313e557..2df3892978 100644 --- a/packages/spec-parser/src/specParser.ts +++ b/packages/spec-parser/src/specParser.ts @@ -45,6 +45,7 @@ export class SpecParser { allowMissingId: true, allowSwagger: true, allowAPIKeyAuth: false, + allowBearerTokenAuth: false, allowMultipleParameters: false, allowOauth2: false, allowMethods: ["get", "post"], @@ -147,7 +148,7 @@ export class SpecParser { for (const auths of authArray) { if (auths.length === 1) { - apiResult.auth = auths[0].authSchema; + apiResult.auth = auths[0].authScheme; break; } } diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index 6eb3bafdcc..97417b501a 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -284,36 +284,23 @@ export class Utils { return false; } - static isSupportedAuth(authSchemaArray: AuthInfo[][], options: ParseOptions): boolean { - if (authSchemaArray.length === 0) { + static isSupportedAuth(authSchemeArray: AuthInfo[][], options: ParseOptions): boolean { + if (authSchemeArray.length === 0) { return true; } - if (options.allowAPIKeyAuth || options.allowOauth2) { + if (options.allowAPIKeyAuth || options.allowOauth2 || options.allowBearerTokenAuth) { // Currently we don't support multiple auth in one operation - if (authSchemaArray.length > 0 && authSchemaArray.every((auths) => auths.length > 1)) { + if (authSchemeArray.length > 0 && authSchemeArray.every((auths) => auths.length > 1)) { return false; } - for (const auths of authSchemaArray) { + for (const auths of authSchemeArray) { if (auths.length === 1) { if ( - !options.allowOauth2 && - options.allowAPIKeyAuth && - Utils.isAPIKeyAuth(auths[0].authSchema) - ) { - return true; - } else if ( - !options.allowAPIKeyAuth && - options.allowOauth2 && - Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema) - ) { - return true; - } else if ( - options.allowAPIKeyAuth && - options.allowOauth2 && - (Utils.isAPIKeyAuth(auths[0].authSchema) || - Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema)) + (options.allowAPIKeyAuth && Utils.isAPIKeyAuth(auths[0].authScheme)) || + (options.allowOauth2 && Utils.isOAuthWithAuthCodeFlow(auths[0].authScheme)) || + (options.allowBearerTokenAuth && Utils.isBearerTokenAuth(auths[0].authScheme)) ) { return true; } @@ -324,12 +311,16 @@ export class Utils { return false; } - static isAPIKeyAuth(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { - return authSchema.type === "apiKey"; + static isBearerTokenAuth(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + return authScheme.type === "http" && authScheme.scheme === "bearer"; + } + + static isAPIKeyAuth(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + return authScheme.type === "apiKey"; } - static isOAuthWithAuthCodeFlow(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { - if (authSchema.type === "oauth2" && authSchema.flows && authSchema.flows.authorizationCode) { + static isOAuthWithAuthCodeFlow(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + if (authScheme.type === "oauth2" && authScheme.flows && authScheme.flows.authorizationCode) { return true; } @@ -350,7 +341,7 @@ export class Utils { for (const name in security) { const auth = securitySchemas[name] as OpenAPIV3.SecuritySchemeObject; authArray.push({ - authSchema: auth, + authScheme: auth, name: name, }); } diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index e26a5979f3..a51d83a030 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -1173,7 +1173,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const apiKeyAuth: AuthInfo = { - authSchema: { + authScheme: { type: "apiKey" as const, name: "api_key_name", in: "header", @@ -1199,6 +1199,81 @@ describe("manifestUpdater", () => { expect(warnings).to.deep.equal([]); }); + it("should contain auth property in manifest if pass the bearer token auth", async () => { + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const adaptiveCardFolder = "/path/to/your/adaptiveCards"; + sinon.stub(fs, "pathExists").resolves(true); + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + composeExtensions: [], + }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: spec.info.title, full: spec.info.description }, + composeExtensions: [ + { + composeExtensionType: "apiBased", + apiSpecificationFile: "spec/outputSpec.yaml", + authorization: { + authType: "apiSecretServiceAuth", + apiSecretServiceAuthConfiguration: { + apiSecretRegistrationId: "${{BEARER_TOKEN_AUTH_REGISTRATION_ID}}", + }, + }, + commands: [ + { + context: ["compose"], + type: "query", + title: "Get all pets", + description: "Returns all pets from the system that the user has access to", + id: "getPets", + parameters: [ + { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + ], + apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", + }, + { + context: ["compose"], + type: "query", + title: "Create a pet", + description: "Create a new pet in the store", + id: "createPet", + parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", + }, + ], + }, + ], + }; + const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); + const bearerTokenAuth: AuthInfo = { + authScheme: { + type: "http" as const, + scheme: "bearer", + }, + name: "bearer_token_auth", + }; + const options: ParseOptions = { + allowMultipleParameters: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const [result, warnings] = await ManifestUpdater.updateManifest( + manifestPath, + outputSpecPath, + spec, + options, + adaptiveCardFolder, + bearerTokenAuth + ); + + expect(result).to.deep.equal(expectedManifest); + expect(warnings).to.deep.equal([]); + }); + it("should contain auth property in manifest if pass the oauth2 with auth code flow", async () => { const manifestPath = "/path/to/your/manifest.json"; const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; @@ -1253,7 +1328,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const oauth2: AuthInfo = { - authSchema: { + authScheme: { type: "oauth2", flows: { authorizationCode: { @@ -1331,7 +1406,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const basicAuth: AuthInfo = { - authSchema: { + authScheme: { type: "http" as const, scheme: "basic", }, @@ -1405,10 +1480,10 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const apiKeyAuth: AuthInfo = { - authSchema: { - type: "apiKey" as const, - name: "key_name", - in: "header", + authScheme: { + type: "http" as const, + scheme: "bearer", + bearerFormat: "JWT", }, name: "*api-key_auth", }; diff --git a/packages/spec-parser/test/specParser.test.ts b/packages/spec-parser/test/specParser.test.ts index a4e14ca72e..7181143051 100644 --- a/packages/spec-parser/test/specParser.test.ts +++ b/packages/spec-parser/test/specParser.test.ts @@ -1819,6 +1819,73 @@ describe("SpecParser", () => { ]); }); + it("should return a list of HTTP methods and paths for all GET with 1 parameter and bearer token auth security", async () => { + const specPath = "valid-spec.yaml"; + const specParser = new SpecParser(specPath, { allowBearerTokenAuth: true }); + const spec = { + components: { + securitySchemes: { + bearerTokenAuth: { + type: "http", + scheme: "bearer", + }, + }, + }, + servers: [ + { + url: "https://server1", + }, + ], + paths: { + "/user/{userId}": { + get: { + security: [{ bearerTokenAuth: [] }], + operationId: "getUserById", + parameters: [ + { + name: "userId", + in: "path", + schema: { + type: "string", + }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + + const result = await specParser.list(); + + expect(result).to.deep.equal([ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { type: "http", scheme: "bearer" }, + operationId: "getUserById", + }, + ]); + }); + it("should return correct auth information", async () => { const specPath = "valid-spec.yaml"; const specParser = new SpecParser(specPath, { allowAPIKeyAuth: true }); diff --git a/packages/spec-parser/test/utils.test.ts b/packages/spec-parser/test/utils.test.ts index 91b6273657..4226bc6734 100644 --- a/packages/spec-parser/test/utils.test.ts +++ b/packages/spec-parser/test/utils.test.ts @@ -305,6 +305,87 @@ describe("utils", () => { assert.strictEqual(result, false); }); + it("should return true if allowBearerTokenAuth is true and contains bearer token auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + components: { + securitySchemes: { + bearer_token1: { + type: "http", + scheme: "bearer", + }, + bearer_token2: { + type: "http", + scheme: "bearer", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + bearer_token2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowBearerTokenAuth: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const result = Utils.isSupportedApi(method, path, spec as any, options); + assert.strictEqual(result, true); + }); + it("should return true if allowAPIKeyAuth is true and contains apiKey auth", () => { const method = "POST"; const path = "/users";