Skip to content

Commit

Permalink
perf(spec-parser): add bearer token auth support (#11098)
Browse files Browse the repository at this point in the history
* perf(spec-parser): add bearer token auth support

* perf: remove unused logic

---------

Co-authored-by: turenlong <rentu@microsoft.com>
  • Loading branch information
SLdragon and SLdragon authored Mar 15, 2024
1 parent d78c2b5 commit 846264f
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 38 deletions.
7 changes: 6 additions & 1 deletion packages/spec-parser/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -243,6 +248,6 @@ export interface ListAPIResult {
}

export interface AuthInfo {
authSchema: OpenAPIV3.SecuritySchemeObject;
authScheme: OpenAPIV3.SecuritySchemeObject;
name: string;
}
5 changes: 2 additions & 3 deletions packages/spec-parser/src/manifestUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
);
Expand Down
1 change: 1 addition & 0 deletions packages/spec-parser/src/specParser.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class SpecParser {
allowSwagger: false,
allowAPIKeyAuth: false,
allowMultipleParameters: false,
allowBearerTokenAuth: false,
allowOauth2: false,
allowMethods: ["get", "post"],
projectType: ProjectType.SME,
Expand Down
3 changes: 2 additions & 1 deletion packages/spec-parser/src/specParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class SpecParser {
allowMissingId: true,
allowSwagger: true,
allowAPIKeyAuth: false,
allowBearerTokenAuth: false,
allowMultipleParameters: false,
allowOauth2: false,
allowMethods: ["get", "post"],
Expand Down Expand Up @@ -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;
}
}
Expand Down
43 changes: 17 additions & 26 deletions packages/spec-parser/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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,
});
}
Expand Down
89 changes: 82 additions & 7 deletions packages/spec-parser/test/manifestUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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";
Expand Down Expand Up @@ -1253,7 +1328,7 @@ describe("manifestUpdater", () => {
};
const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest);
const oauth2: AuthInfo = {
authSchema: {
authScheme: {
type: "oauth2",
flows: {
authorizationCode: {
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
};
Expand Down
67 changes: 67 additions & 0 deletions packages/spec-parser/test/specParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
Loading

0 comments on commit 846264f

Please sign in to comment.